Throttle and Debounce

December 13, 2023

  • typescript
有任何問題,我能幫上忙的話都能到 github 發 issue 問我。

Confuse

Throttle 跟 Debounce,究竟要如何區分呢?我相信很多剛接觸程式的工程師會搞混,我自己是這樣記的:

Throttle 是閥門的意思,可以把它想像成是日本庭院常常出現的流水竹筧,他的概念就是水太多時,他會將竹筧內的水倒到池子內,接著又回復原位,週而復始,當水注入的量很穩定,就會每隔一段準確的時間將水倒入池子內,定時地每隔一段時間就去做觸發最新一次的東西,減緩 function 執行的太過頻繁,使用場景會像是滑鼠滾動或著瀏覽器頁面 resize 時,可以拿來做性能優化使用。

Debounce 就是防手抖的意思,你可以想像你在短時間內抖了很多下,每次觸發都會去重新註冊一個 wait 等待的 timeout,你在等待的時間內若沒再次觸發,程式才會執行。使用場景會像是 google 的搜尋並顯示下方 dropdown 的功能,使用者輸入很快,但當使用者停下幾毫秒後 dropdown 才會顯示,並秀出所搜尋的結果。

那為何這兩樣東西很重要,最主要的點,在於它可以有效緩解伺服器 io 的問題,且使用者體驗也不會有所降低。

Knowledge

在進入程式碼撰寫的部分前,有些 js 的基礎你可能要先知道。

js 有 fn.applyfn.callfn.bind,簡而言之就是你希望這個 function 能立即執行,請使用 fn.applyfn.call,而 fn.bind 則是可以後續你 call 時再執行。這點如果讀者如果有 React class component 的經驗應該會比較熟悉,每次註冊完事件都要 bind this,讓他跟這個 React.Component 有掛勾,不然 this 值會是 undefined,畢竟沒法寫 arrow function。

可以參考一下 Dan 在 react conf 上的演講,他就親自示範忘記加 fn.bind 的情況 🤣。


fn.applyfn.call 的差別又是什麼呢? 差別在於 fn.apply 的第二個參數是 array-like objectfn.call 是 arg1, arg2…, argN 的寫法,所以主要取決在你的參數是如何。

// fn.apply
const obj = { num: 10 }

function add(a, b) {
  return this.num + a + b
}

const args = [5, 7]
const result = add.apply(obj, args)
// fn.call
const obj = { num: 10 }

function add(a, b) {
  return this.num + a + b
}

const result = add.call(obj, 5, 7)

Coding Debounce

我們來實作一下簡單的 debounce,在因為 wait 時間未到,導致程式還未被觸發時,你又再次觸發,便會清掉上次的 timeout 及預計要執行的程式,達到上次預計要執行的動作被 abort 掉的功能。

下面有附上例圖及影片。

Video from: https://www.webdong.dev/post/learn-debounce-and-throttle

debounce

Image from: https://rxjs.dev/api/index/function/debounceTime

// debounce
function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeoutId: ReturnType<typeof setTimeout>

  return function (this: any, ...args: Parameters<T>) {
    const context = this

    clearTimeout(timeoutId) // abort prev timeout

    // 當 wait 的時間到,使用 func.apply 執行該 method
    timeoutId = setTimeout(() => func.apply(context, args), wait)
  }
}

function exampleFunction(input: string) {
  console.log(`Input received: ${input}`)
}

const debouncedExampleFunction = debounce(exampleFunction, 500)

Coding Throttle

接著我們來實作一下 Throttle,這個就稍微比較複雜一點,基本上可以想像成是裡面有一個開關 isThrottled,我們透過這個開關在操縱程式是否被執行,預設是 false,執行完 func.apply() 後,isThrottled 便會修改為 true 及 setTimeout 會被觸發,但尚未執行,在 setTimeout 尚未執行的期間,只要是 isThrottled 是 true 的情況下原先的 args 就會被覆蓋掉,不會被觸發,當 setTimeout 裡的程式被更改為 false 時,wrapper.apply() 遞迴會被執行,該過程也間接執行 func.apply(),接著又進入下一輪的巡迴。

下面有附上例圖及影片。

Video from: https://www.webdong.dev/post/learn-debounce-and-throttle

throttle

Image from: https://rxjs.dev/api/index/function/throttleTime

// throttle
function throttle<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let isThrottled = false
  let savedArgs: Parameters<T> | null
  let savedContext: any

  function wrapper(this: any, ...args: Parameters<T>) {
    // throttle 為 true 時才會貯存參數
    if (isThrottled) {
      savedArgs = args
      savedContext = this
      return
    }

    // isThrottled -> false 情況下才會用 func.apply 去觸發帶入的 function
    func.apply(this, args)
    isThrottled = true

    setTimeout(() => {
      isThrottled = false
      if (savedArgs) {
        wrapper.apply(savedContext, savedArgs) // 遞迴地去 call 自己
        savedArgs = null
        savedContext = null
      }
    }, wait)
  }

  return wrapper as (...args: Parameters<T>) => void
}

function exampleFunction(input: string) {
  console.log(`Input received: ${input}`)
}

const throttledExampleFunction = throttle(exampleFunction, 500)

// This will execute immediately and then at most once every 500ms
throttledExampleFunction("Hello")
throttledExampleFunction("Throttle")
throttledExampleFunction("Function")

Conclusion

此篇,記錄一下自己對 Debounce 及 Throttle 的理解,加深印象,外加有時候寫這個也滿好玩的,畢竟現在基本上也不用自己造輪子,引用下 vueuse,lodash,react-use,可以簡化很多東西。

可能等工作跟自己的事情忙完,下一篇有點想寫 Chrome Extension manifest v3 的一點東西,期許自己能把文章生出來吧。


Mayvis Chen
你好,我是 Mayvis Chen
住在台北,會點前端,會點後端,熱愛分享新知及愛喝奶茶的全端打雜工程師。