iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
自我挑戰組

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

[Day 18] useDebounceFn

  • 分享至 

  • xImage
  •  

鐵人賽一開始是用 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 出去的 filterfilter 中的 invoke 被執行的時候,就是我們傳給 useDebounceFn 的第一個參數 fn 被執行。細節部分可以再參考之前 useThrottleFn 的文章。

GitHub:https://github.com/RhinoLee/30days_vue/pull/17/commits/a8df732929ba9cb83fb9531fdddd419367186bfc

核心邏輯 - maxWait 參數

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


上一篇
[Day 17] useParallax - 官網文件 Demo 效果實作
下一篇
[Day 19] useDebounceFn - unit test
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言