今天來寫 throttle 的核心邏輯,會先從只處理 fn, delay 參數的版本開始實作,接著加入 trailing, leading 的判斷。另外 vueuse 有一個 rejectOnCancel 的參數,加入這個參數後程式碼會比較複雜,這個就留到明天繼續。
// 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 為 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 為 true 的時候,在 throttle 週期開始會立即呼叫,舉例來說,前ㄧ次跟這次點擊的時間差,超過設定的 ms 時間,這次點擊就會被視為 throttle 週期的開始。
假設我們設置 throttle 時間為 1000ms:
前面的版本就已經有這種特性了,所以這邊是要實作 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:
isLeading
設定回 true。isLeading
已經設定回 true,這時候這個「機會」就消失了,也因為這樣的特性,可以達成 leading 為 false 的效果
接著來說這個「機會」什麼時候會出現
假設我們設置 throttle 時間為 1000ms:
isLeading
設定回 true。isLeading
設定回 true。isLeading
設定回 true。重點在這時候 duration 已經 >= ms 了,isLeading
也還在 false 的狀態,所以在這個「機會」之下,可以成功觸發到 fn
GitHub:https://github.com/RhinoLee/30days_vue/pull/5/files