函數節流與函數防抖

  • 2019 年 12 月 5 日
  • 筆記

函數節流與函數防抖

函數節流和函數防抖是一個老生常談的話題了,兩者都是對大量頻繁重複調用代碼的一種優化方案 今天在某群和大家討論時,順便搜了一些相關博客 發現有一篇關於兩者的定義竟然寫反了。。。所以決定自己來寫一下-.-權當加深記憶了

函數節流(throttle)

正如其命名的含義,節流。 限制函數在一定時間內調用的次數。

類似的實際生活中的場景

早晚高峰的地鐵排隊。

太多的人擁擠到站台上,大家都想搭上這班車,人擠人之間,難免會出現一些問題。 所以在很多地鐵站,高峰期會設置很多層的屏障,來增加你進站的時間,從而減少站台的壓力。

在程序中的實踐

同理,代入程序中,我們可以通過限制函數調用的頻率,來抑制資源的消耗。 比如我們要實現一個元素拖拽的效果,我們是可以在每次move事件中都進行重繪DOM,但是這樣做,程序的開銷是非常大的。 所以在這裡我們可以用到函數節流的方法,來減少重繪的次數:

// 普通方案  $dragable.addEventListener('mousemove', () => {    console.log('trigger')  })    // 函數節流的實現方案  let throttleIdentify = 0  $dragable.addEventListener('mousemove', () => {    if (throttleIdentify) return    throttleIdentify = setTimeout(() => throttleIdentify = 0, 500)    console.log('trigger')  })

這樣來做的話,在拖動的過程中,能保證500ms內,只會重繪一次DOM。 在同時監聽了mouseover後,兩者最終的效果是一致的,但是在拖動的過程中,函數節流版觸發事件的次數會減少很多,遂消耗更少的資源。

通用的函數節流實現

/**   * 函數節流的實現   * @param  {Function} func      要實現函數節流的原函數   * @param  {Number}   interval  節流的間隔   * @return {Function}           添加節流功能的函數   */  function throttle (func, interval) {    let identify = 0    return (...args) => {      if (identify) return      identify = setTimeout(() => identify = 0, interval)      func.apply(this, args)    }  }

類似函數節流的操作

平時開發中經常會做的ajax請求獲取數據,這裡可以用到類似函數節流的操作。 在我們發送一個請求到後台時,當返回的數據還沒有接收到時,我們會添加一個標識,來表明當前有一個請求正在被處理,如果這時用戶再觸發ajax請求,則會直接跳過本次函數的執行。 同樣的還有滑動加載更多數據,如果不添加類似的限制,可能會導致發送多條請求,渲染重複數據。

我曾經在某軟件里遇到過-.-連續點擊登錄按鈕數十次,結果連着給我彈了三次密碼錯誤,隨後告訴我輸入密碼錯誤超過三次,您的賬號已被鎖定。

函數節流的定義:限制函數在一定時間內調用的次數

函數防抖(debounce)

是函數在特定的時間內不被再調用後執行。

實際的例子

還是拿城市交通工具來說事兒。。 坐公交時,到站了,是由司機來操作車門的開合的。 當司機準備離站時,關閉車門,這是突然跑過來一人,司機只好再將車門打開,讓人上來。

又或者,乘坐升降電梯時,電梯門關閉後,外邊跑來一人又將電梯門按開。 這兩件事兒都是因為關門這一個事件處理太快導致的,徒增一次開關門的消耗。

在程序中的實踐

監聽窗口大小重繪的操作。

在用戶拖拽窗口時,一直在改變窗口的大小,如果我們在resize事件中進行一些操作,消耗將是巨大的。 而且大多數可能是無意義的執行,因為用戶還處於拖拽的過程中。 所以我們可以用函數防抖來優化相關的處理

// 普通方案  window.addEventListener('resize', () => {    console.log('trigger')  })    // 函數防抖方案  let debounceIdentify = 0  window.addEventListener('resize', () => {    debounceIdentify && clearTimeout(debounceIdentify)    debounceIdentify = setTimeout(() => {      console.log('trigger')    }, 300)  })

我們在resize事件中添加了一個300ms的延遲執行邏輯。 並且在每次事件觸發時,都會重新計時,這樣做也就可以保證,函數的執行肯定是在距離上次resize事件被觸發的300ms後。 兩次resize事件間隔小於300ms的都會被忽略,這樣就節省了很多無意義的事件觸發。

輸入框的輸入聯想

幾乎所有的搜索引擎都會對你輸入的文字進行預判,並在下方推薦相關的結果。 但是這個聯想意味着我們需要將當前用戶所輸入的文本傳遞到後端,並獲取返回數據,展示在頁面中。 如果遇到打字速度快的人,比如260字母/分鐘的我,在一小段時間內,會連續發送大量的ajax請求到後端。 並且當前邊的數據返回過來後,其實已經失去了展示的意義,因為用戶可能從you輸入到了young,這兩個單詞相關的結果肯定是不一樣的。 所以我們就可以在監聽用戶輸入的事件那裡做函數防抖的處理,在XXX秒後發送聯想搜索的ajax請求。

通用的函數防抖實現

/**   * 函數防抖的實現   * @param  {Function} func   要實現函數節流的原函數   * @param  {Number}   delay  結束的延遲時間   * @return {Function}        添加節流功能的函數   */  function debounce (func, delay) {    let debounceIdentify = 0    return (...args) => {      debounceIdentify && clearTimeout(debounceIdentify)      debounceIdentify = setTimeout(() => {        debounceIdentify = 0        func.apply(this, args)      }, delay)    }  }

類似函數防抖的操作

在一些與用戶的交互上,比如提交表單後,一般都會顯示一個loading框來提示用戶,他提交的表單正在處理中。 但是發送表單請求後就顯示loading是一件很不友好的事情,因為請求可能在幾十毫秒內就會得到響應。 這樣在用戶看來就是頁面中閃過一團黑色,所以可以在提交表單後添加一個延遲函數,在XXX秒後再顯示loading框。 這樣在快速響應的場景下,用戶是不會看到一閃而過的loading框,當然,一定要記得在接收到數據後去clearTimeout

let identify = setTimeout(showLoadingModal, 500)  fetch('XXX').then(res => {    // doing something      // clear timer    clearTimeout(identify)  })

函數防抖的定義:函數在特定的時間內不被再調用後執行

總結

函數節流、函數防抖 兩者都是用來解決代碼短時間內大量重複調用的方案。 當然,也是各有利弊。 在實際開發中,兩者的界定也很模糊。 比如搜索關鍵字聯想,用節流或者防抖都可以來做,拖拽DOM、監聽resize等等,這兩個都是可以來實現的。

兩者的區別在於: 函數節流在一定時間內肯定會觸發多次,但是最後不一定會觸發 函數防抖可能僅在最後觸發一次

記住上邊這兩句,我覺得遇到類似需要進行優化的場景,應該就能夠知道該用哪個了。

參考資料

  1. Javascript debounce vs throttle function
  2. Javascript function debounce and throttle