iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0
自我挑戰組

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

[Day 5] useThrottleFn - Promise & rejectOnCancel

  • 分享至 

  • xImage
  •  

今天要加入 rejectOnCancel 這個參數,會從昨天調整好的 code 繼續,為了方便觀看,可以另開分頁到 Day 4 的最新進度區塊,看最新版本的程式碼~

調整 Demo code,要在 throttle 取消執行 fn 的時候可以 catch 到 error

<!-- 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>

調整核心邏輯 throttleFilter

// 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。

調整 createFilterWrapper

// 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 來結束這回合~


上一篇
[Day 4] useThrottleFn - 加入 rejectOnCancel 之前,先來處理 this
下一篇
[Day 6] useThrottleFn - unit test & setTimeout 傳入超大負數導致的 bug
系列文
30 天 vueuse 原始碼閱讀與實作13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言