iT邦幫忙

2024 iThome 鐵人賽

DAY 3
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 3

[Day 3] useThrottleFn - 核心邏輯

  • 分享至 

  • xImage
  •  

今天來寫 throttle 的核心邏輯,會先從只處理 fn, delay 參數的版本開始實作,接著加入 trailing, leading 的判斷。另外 vueuse 有一個 rejectOnCancel 的參數,加入這個參數後程式碼會比較複雜,這個就留到明天繼續。

只處理 fn, delay 參數

// src/utils/filter.js
export function throttleFilter(fn, ms) {
  let lastExec = 0
  let timer = null

  return function () {
    const duration = Date.now() - lastExec

    if (timer) {
      clearTimeout(timer)
      timer = null
    }

    if (duration >= ms) {
      lastExec = Date.now()
      fn()
    }
    else {
      timer = setTimeout(() => {
        lastExec = Date.now()
        fn()
        clearTimeout(timer)
        timer = null
      }, ms - duration)
    }
  }
}

這邊使用 closure 來讓 return 出去的 function 每次執行時,都可以拿到上一次執行時更新的 lastExec(最後執行時間) ,因此可以拿當前時間減掉 lastExec 來得到兩次觸發之間的時間間隔(duration)。

有了 duration 就可以判斷上次執行到這次執行中間的時間,是否有超過設定的 ms 毫秒數,有超過的話直接執行 fn。

沒有超過就不能直接執行,要經過 (ms - duration) ms 後才能執行,舉例來說,假設 delay 設定為 2000 ms,在第一次點擊後經過了 1100 ms 點擊第二次,這時候就會等待 900 毫秒才會執行 fn,因為這種計算方式,假設不規則連點(每次間隔都 < 2000ms),也依樣能維持穩定 2000ms 才執行 fn。

trailing

當 trailing 為 true 時,在快速連續事件結束後的最後一次事件一定會被處理。前面的版本就已經有這種特性了,所以這邊是要實作 trailing 為 false 的效果。

export function throttleFilter(fn, ms, trailing = true) {
    // ...略
    if (duration >= ms) {
      lastExec = Date.now()
      fn()
    }
    else if (trailing) {
      timer = setTimeout(() => {
        // ...略
      }, ms - duration)
    }
}

加上參數 trailing 並預設為 true(原始碼中預設是 false,這邊測試方便所以先設定為 true),剩下的其實就只有把原本 else 的部分改成 else if (trailing)
換句話說就是 trailing 為 false 時,不會處理 throttle 期間的呼叫。舉例來說,如果我們設置 ms 為 1000 毫秒,trailing 為 false,然後在 0ms、200ms、400ms、600ms 時呼叫函數,只有 0ms 的呼叫會被執行,如果是 trailing 為 true 的情境,600ms 這次的呼叫也會被執行。

leading

當 leading 為 true 的時候,在 throttle 週期開始會立即呼叫,舉例來說,前ㄧ次跟這次點擊的時間差,超過設定的 ms 時間,這次點擊就會被視為 throttle 週期的開始。

假設我們設置 throttle 時間為 1000ms:

  • 在 0ms 時呼叫函數,它會立即執行(因為 leading 為 true)。
  • 在 500ms 時再次呼叫,由於未超過 1000ms,這次呼叫不屬於 throttle 週期的開始。
  • 在 1200ms 時再次呼叫,因為已經超過了 1000ms,這次呼叫會被視為新的 throttle 週期的開始,並立即執行。

前面的版本就已經有這種特性了,所以這邊是要實作 leading 為 false 的效果。

export function throttleFilter(fn, ms, trailing = false, leading = false) {
  // ...略
  let isLeading = true

  return function () {
    // ...略

    if (duration >= ms && (leading || !isLeading)) {
      lastExec = Date.now()
      fn()
    }
    else if (trailing) {
      // ...略
    }

    if (!leading && !timer) {
      timer = setTimeout(() => {
        isLeading = true
      }, ms)
    }

    isLeading = false
  }
}

建議把 trailing 調整為 false,比較好觀察 leading 為 false 的效果(原始碼 leading 預設為 true)。

關鍵應該是 isLeading 這個變數的控制,在函數執行完的最後有一個 isLeading = false,可以想成要給個「機會」讓 if (duration >= ms && (leading || !isLeading)) 這個判斷式有辦法通過,因為如果一直無法通過的話,那永遠都不會執行 fn 了。那這個「機會」從何而來,又會在什麼時候消失呢?

先講什麼時候這個「機會」會消失,可以看到有一段 setTimeout 來把 isLeading 設定回 true。
假設我們設置 throttle 時間為 1000ms:

  • 在 0ms 時呼叫函數,setTimeout 延遲 1000ms 把 isLeading 設定回 true。
  • 在 1200ms 時再次呼叫,因為已經超過了 1000ms,在 1000ms 的時候 isLeading 已經設定回 true,這時候這個「機會」就消失了,也因為這樣的特性,可以達成 leading 為 false 的效果

接著來說這個「機會」什麼時候會出現
假設我們設置 throttle 時間為 1000ms:

  • 在 0ms 時呼叫函數,setTimeout 延遲 1000ms 把 isLeading 設定回 true。
  • 在 800ms 時呼叫函數,清掉原本的 timeout,重新設定 setTimeout 延遲 1000ms 把 isLeading 設定回 true。
  • 在 1100ms 時呼叫函數,清掉原本的 timeout,重新設定 setTimeout 延遲 1000ms 把 isLeading 設定回 true。重點在這時候 duration 已經 >= ms 了,isLeading 也還在 false 的狀態,所以在這個「機會」之下,可以成功觸發到 fn

GitHub:https://github.com/RhinoLee/30days_vue/pull/5/files


上一篇
[Day 2] useThrottleFn - 序章
下一篇
[Day 4] useThrottleFn - 加入 rejectOnCancel 之前,先來處理 this
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言