鐵人賽一開始是用 useThrottleFn 起手,原本接下來打算寫 useDebounceFn,但因為性質太像有點膩 XD,於是轉而去研究 useParallax。不過最後還是無法逃過命運,因為 useInfiniteScroll 會用到 useScroll,而 useScroll 又依賴 useDebounceFn。
這次應該不會再有什麼「序章」來水了(笑),就直接來看 useDebounceFn 吧!
在寫 useThrottleFn 的時候,有把步驟拆比較細,這次寫 useDebounceFn 重複的部分就不拆了,像是 Promise 的部分會直接一起看,要看細節的話可以參考 Day3 ~ Day6。
官方 Demo:https://vueuse.org/shared/useDebounceFn/#usedebouncefn
一般來說使用 debounce,假設我們 ms 傳入 1000,會在連續觸發的最後一次觸發的時間點,往後算 1000ms 才會執行我們傳入的 fn,這中間 fn 是沒有機會被執行的。可以看到官方 Demo 有一個 { maxWait: 5000 }
的參數,用途是當連續觸發時間等於 5000ms 時,會強制執行 fn,假設我們連續觸發 10s,那 fn 就會每 5000ms 執行一次。
接下來會先實作基本功能,暫時不加入 maxWait 的判斷。
// src/utils/filter.js
export function debounceFilter(ms, options) {
let timer
let lastRejector
const _clearTimeout = (timer) => {
clearTimeout(timer)
lastRejector()
lastRejector = noop
}
const filter = (invoke) => {
const duration = toValue(ms)
// 每次 filter 被觸發,就要先清空 timer,重新計算 timeout,因為只需要拿最後一次的 setTimeout 執行
if (timer)
_clearTimeout(timer)
if (duration <= 0) {
return Promise.resolve(invoke())
}
return new Promise((resolve, reject) => {
// 如果 rejectOnCancel option 有設定為 true,連續觸發期間的每一次觸發都會 reject,可以在上層 catch 到
lastRejector = options.rejectOnCancel ? reject : resolve
timer = setTimeout(() => {
resolve(invoke())
}, duration)
})
}
return filter
}
// src/helper.js
export function noop() {}
export function toValue(r) {
return typeof r === 'function'
? r()
: unref(r)
}
這個結構跟之前看的 useThrottleFn 差不多,但邏輯筆 throttle 簡單很多,這邊前情提要一個重點就好,其他就用註解的方式寫在程式碼中,先看用法:
const debouncedFn = useDebounceFn(() => {
// do something
}, 1000)
window.addEventListener('resize', debouncedFn)
debouncedFn 每次被觸發的時候,都會執行到 debounceFilter
return 出去的 filter
,filter
中的 invoke
被執行的時候,就是我們傳給 useDebounceFn 的第一個參數 fn 被執行。細節部分可以再參考之前 useThrottleFn 的文章。
GitHub:https://github.com/RhinoLee/30days_vue/pull/17/commits/a8df732929ba9cb83fb9531fdddd419367186bfc
// src/utils/filter.js
export function debounceFilter(ms, options) {
let timer
let maxTimer
let lastRejector
const _clearTimeout = (timer) => {
clearTimeout(timer)
lastRejector()
lastRejector = noop
}
const filter = (invoke) => {
const duration = toValue(ms)
const maxDuration = toValue(options.maxWait)
// 每次 filter 被觸發,就要先清空 timer,重新計算 timeout,因為只需要拿最後一次的 setTimeout 執行
if (timer)
_clearTimeout(timer)
// maxDuration 會在時間到時強制觸發
if (duration <= 0 || (maxDuration !== undefined && maxDuration <= 0)) {
if (maxTimer) {
_clearTimeout(maxTimer)
maxTimer = null
}
return Promise.resolve(invoke())
}
return new Promise((resolve, reject) => {
lastRejector = options.rejectOnCancel ? reject : resolve
/**
* 如果有傳入 > 0 的 maxWait,而且還沒設定過 maxTimer,就設定一個 maxTimer,時間就是用傳入的 maxWait option
* 在 maxTimer 到點要執行 callback 的時候,也有可能有一般的 timer 還在倒數,舉個例子:
* 在 ms 設定 1000 的情況下,我的最後一次觸發就會讓一般的 timer 開始倒數,只要 1000ms 到了,就會執行傳入的 fn
* 但如果 1000ms 還沒到,我再觸發下一次,注意下面程式碼,一般 timer 一定會被設置,所以這邊需要針對一般 timer 做清除
*/
if (maxDuration && !maxTimer) {
maxTimer = setTimeout(() => {
if (timer)
_clearTimeout(timer)
maxTimer = null
resolve(invoke())
}, maxDuration)
}
timer = setTimeout(() => {
// 在一般計時器到點後,要清除 maxTimer,否則會出現 maxTimer 到點後再次執行的非預期狀況
if (maxTimer)
_clearTimeout(maxTimer)
maxTimer = null
resolve(invoke())
}, duration)
})
}
return filter
}
因為有點難把 maxWait 的部分拆出來講,所以我把相關心得用註解的方式寫在程式碼中,跟基礎功能的差異可以參考 GitHub:https://github.com/RhinoLee/30days_vue/pull/17/commits/86235a40fabf8f7ad2feb1cd7104d988b04b57e3
因為要實作 maxWait,所以有 timer 跟 maxTimer 這種雙 timer 的設計還滿有趣的,不過相對的要很注意 clearTimout 的時機。
今天就到這邊告一段落,明天會繼續看 debounceFilter 相關的 unit test~