今天要加入 rejectOnCancel 這個參數,會從昨天調整好的 code 繼續,為了方便觀看,可以另開分頁到 Day 4 的最新進度區塊,看最新版本的程式碼~
<!-- src/components/UseThrottleFnDemo.vue -->
<script setup>
// ...略
// traling 要是 true,rejectOnCancel 才會有效果
const throttledFn = useThrottleFn(updateValue, 1000, true, true, true)
function clickHandler() {
clicked.value += 1
const testThis = {}
try {
throttledFn.apply(testThis)
}
catch {
// 連續觸發時,上一次觸發被 throttle cancel 掉,目標是可以在這邊獲得通知
console.error('cancel')
}
}
</script>
// src/utils/filter.js
export function throttleFilter(ms, trailing = false, leading = false, rejectOnCancel = false) {
const noop = () => {}
let lastExec = 0
let timer
let isLeading = true
let lastValue
let lastRejector = noop
const clear = () => {
if (timer) {
clearTimeout(timer)
timer = undefined
lastRejector()
lastRejector = noop
}
}
return function (_invoke) {
const duration = Date.now() - lastExec
const invoke = () => {
return lastValue = _invoke()
}
clear()
if (duration >= ms && (leading || !isLeading)) {
lastExec = Date.now()
invoke()
}
else if (trailing) {
lastValue = new Promise((resolve, reject) => {
lastRejector = rejectOnCancel ? reject : resolve
timer = setTimeout(() => {
lastExec = Date.now()
resolve(invoke())
clear()
}, ms - duration)
})
}
if (!leading && !timer) {
timer = setTimeout(() => {
isLeading = true
}, ms)
}
isLeading = false
return lastValue
}
}
先假設我們設定的 ms 是 3000ms。
可以看到 rejectOnCancel 只有在 trailing 為 true 的時候才會有效果,應該滿合理的,因為如果 trailing 是 false,只會在觸發時間差 3000ms 以上才會執行,也沒有東西可以讓我們取消。
所謂的取消指的是當我們觸發第一次,經過 3000ms 之前中間的任何點擊(除了最後一下)都會被算成是取消,接下來會把這段時間定義為「取消期間」會比較好說明,在「取消期間」點擊的最後一下會在 3000ms 時間到的時候觸發,就是前幾天講的 trailing 為 true 的效果。
這次為了加入 rejectOnCancel 多出了幾個東東(之後會統一用 rejectOnCancel = true 的情境講解),關鍵可以先聚焦在這個地方
// src/utils/filter.js
// ...略
const clear = () => {
if (timer) {
clearTimeout(timer)
timer = undefined
lastRejector()
lastRejector = noop
}
}
return function (_invoke) {
// ...略
clear()
// ...略
else if (trailing) {
lastValue = new Promise((resolve, reject) => {
lastRejector = rejectOnCancel ? reject : resolve
timer = setTimeout(() => {
lastExec = Date.now()
resolve(invoke())
clear()
}, ms - duration)
})
}
// ...略
在「取消期間」會把 lastRejector 設定為 reject,每次觸發的時候都會執行 clear(),也就會執行 lastRejector(),這時候上層如果有 try catch,就可以在 catch 區塊接收到被取消通知。
另外講一下「取消期間」的最後一次點擊,因為在這邊我卡住一陣子 XD 我本來把 resolve(invoke())
寫成 invoke()
,導致最後一次點擊確實會生效,但後面會再 catch 到一次取消通知,造成的原因是 Promise 的基本原理,因為我寫成 invoke()
,所以這個 Promise 並沒有被 resolve,而後面馬上又接了一個 clear(),Promise 的狀態這時候會從 pending 變成 rejected 狀態,所以被 catch 到。
改成 resolve(invoke())
後,Promise 的狀態會從 pending 變成 fulfilled,基於 Promise 狀態不可逆,所以就算後面接了 clear(),也不會再變成 rejected。
// src/utils/filter.js
export function createFilterWrapper(filter, fn) {
function wrapper(...args) {
return new Promise((resolve, reject) => {
Promise.resolve(filter(() => fn.apply(this, args), { fn, this: this, args }))
.then(resolve)
.catch(reject)
})
};
return wrapper
}
昨天有提到 Demo code 中的 throttledFn function 就是 createFilterWrapper return 出去的 wrapper,lastValue 在觸發時間差超過我們設定的 3000ms 時,是 invoke() 的結果(也就是我們傳入的 fn 執行的結果),如果是在 trailing 中觸發,lastValue 會被設定為 Promise,這個 lastValue 同時也是 filter() 執行後的回傳值,我猜這邊是為了讓 filter() 回傳值統一,所以無論如何都包裝成 Promise。
GitHub:https://github.com/RhinoLee/30days_vue/pull/7/files
以上就是今天的內容了,不過寫到最後還是想不太到 rejectOnCancel 的實際應用場景 XD
明天就會用 useThrottleFn 相關的 unit test 來結束這回合~