事件的節(jié)流(throttle)與防抖(debounce)

問題的引出

在一些場景往往由于事件頻繁被觸發(fā),因而頻繁地進(jìn)行DOM操作、資源加載,導(dǎo)致UI停頓甚至瀏覽器崩潰。
在這樣的情況下,我們實(shí)際上的需求大多為停止改變大小n毫秒后執(zhí)行后續(xù)處理;而其他事件大多的需求是以一定的頻率執(zhí)行后續(xù)處理。針對這兩種需求就出現(xiàn)了debounce和throttle兩種解決辦法。

前緣

我首次看到 debounce 的 JavaScript 實(shí)現(xiàn)是在 2009 年的 John Hann 的博文 。
不久后,Ben Alman 做了個 jQuery 插件 (不再維護(hù)),一年后 Jeremy Ashkenas 把它 加入了 underscore.js 。而后加入了 Lodash 。

Lodash 給 _.debounce_.throttle添加了 不少特性 。之前的 immediate( underscore.js 中用) 被 leading(最前面) 和 trailing(最后面) 選項(xiàng)取代。你可以選一種,或者都選,默認(rèn)只有 trailing啟用。

新的 maxWait選項(xiàng)(僅 Lodash 有)本文未提及,但是也很有用。事實(shí)上,throttle 方法是用 _.debouncemaxWait實(shí)現(xiàn)的,你可以看 lodash 源碼

See the Pen New example by Corbacho ( @dcorb ) on CodePen .

防抖( Debounce )和節(jié)流( throttle )都是用來控制某個函數(shù)在一定時間內(nèi)執(zhí)行多少次的技巧,兩者相似而又

throttle(節(jié)流)

有一個調(diào)用周期,在一個很長的時間里分為多段,每一段執(zhí)行一次。例如onscroll,resize,500ms執(zhí)行一次

調(diào)整大小的例子

調(diào)整桌面瀏覽器窗口大小的時候,會觸發(fā)很多次 resize事件。
看下面 demo:

See the Pen Debounce Resize Event Example by Corbacho ( @dcorb ) on CodePen .

如你所見,我們?yōu)?resize 事件使用了默認(rèn)的 trailing選項(xiàng),因?yàn)槲覀冎魂P(guān)心用戶停止調(diào)整大小后的最終值。

基于 AJAX 請求的自動完成功能,通過 keypress 觸發(fā)

為什么用戶還在輸入的時候,每隔50ms就向服務(wù)器發(fā)送一次 AJAX 請求? _.debounce可以幫忙,當(dāng)用戶停止輸入的時候,再發(fā)送請求。
此處也不需要 leading標(biāo)記,我們想等最后一個字符輸完。

See the Pen Debouncing keystrokes Example by Corbacho ( @dcorb ) on CodePen .

相似的使用場景還有,直到用戶輸完,才驗(yàn)證輸入的正確性,顯示錯誤信息。

debounce(防抖)

在一定時間內(nèi)不調(diào)用,只調(diào)用一次。例如input事件,停止輸入500ms之后再執(zhí)行。

無限滾動

用戶向下滾動無限滾動頁面,需要檢查滾動位置距底部多遠(yuǎn),如果鄰近底部了,我們可以發(fā) AJAX 請求獲取更多的數(shù)據(jù)插入到頁面中。
我們心愛的 _.debounce就不適用了,只有當(dāng)用戶停止?jié)L動的時候它才會觸發(fā)。只要用戶滾動至鄰近底部時,我們就想獲取內(nèi)容。
使用 _.throttle可以保證我們不斷檢查距離底部有多遠(yuǎn)。

See the Pen Infinite scrolling throttled by Corbacho ( @dcorb ) on CodePen .

如何使用 debounce 和 throttle 以及常見的坑

自己造一個 debounce / throttle 的輪子看起來多么誘人,或者隨便找個博文復(fù)制過來。 我是建議直接使用 underscore 或 Lodash 。如果僅需要 _.debounce_.throttle 方法,可以使用 Lodash的自定義構(gòu)建工具,生成一個 2KB 的壓縮庫。使用以下的簡單命令即可:

npm i --save lodash
const { debounce, throttle  } = require('lodash');

常見的坑是,不止一次地調(diào)用 _.debounce 方法:

// 錯誤
$(window).on('scroll', function() {
   _.debounce(doSomething, 300); 
});

// 正確
$(window).on('scroll', _.debounce(doSomething, 200));

debounce 方法保存到一個變量以后,就可以用它的私有方法 debounced_version.cancel()lodashunderscore.js 都有效。

var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);

// 如果需要的話
debounced_version.cancel();

requestAnimationFrame(rAF)

requestAnimationFrame是另一種限速執(zhí)行的方式。跟 _.throttle(dosomething, 16)等價(jià)。它是高保真的,如果追求更好的精確度的話,可以用瀏覽器原生的 API 。
可以使用 rAF API 替換 throttle 方法,考慮一下優(yōu)缺點(diǎn):

優(yōu)點(diǎn)

  • 動畫保持 60fps(每一幀 16 ms),瀏覽器內(nèi)部決定渲染的最佳時機(jī)
  • 簡潔標(biāo)準(zhǔn)的 API,后期維護(hù)成本低

缺點(diǎn)

  • 動畫的開始/取消需要開發(fā)者自己控制,不像 ‘.debounce’ 或 ‘.throttle’由函數(shù)內(nèi)部處理。
  • 瀏覽器標(biāo)簽未激活時,一切都不會執(zhí)行。
  • 盡管所有的現(xiàn)代瀏覽器 都支持 rAF ,IE9,Opera Mini 和 老的 Android 還是 需要打補(bǔ)丁 。
  • Node.js 不支持,無法在服務(wù)器端用于文件系統(tǒng)事件。

根據(jù)經(jīng)驗(yàn),如果 JavaScript 方法需要繪制或者直接改變屬性,我會選擇 requestAnimationFrame,只要涉及到重新計(jì)算元素位置,就可以使用它。

涉及到 AJAX 請求,添加/移除 class (可以觸發(fā) CSS 動畫),我會選擇 _.debounce或者 _.throttle,可以設(shè)置更低的執(zhí)行頻率(例子中的200ms 換成16ms)。

rAF 實(shí)例

靈感來自于 Paul Lewis 的文章 ,我將用 requestAnimationFrame 控制 scroll 。
16ms 的 _.throttle拿來做對比,性能相仿,用于更復(fù)雜的場景時,rAF 可能效果更佳。

See the Pen Scroll comparison requestAnimationFrame vs throttle by Corbacho ( @dcorb ) on CodePen .
headroom.js 是個更 高級的例子 。

結(jié)論

使用 debounce,throttle 和 requestAnimationFrame 都可以優(yōu)化事件處理,三者各不相同,又相輔相成。

總之:

  • debounce :把觸發(fā)非常頻繁的事件(比如按鍵)合并成一次執(zhí)行。
  • throttle :保證每 X 毫秒恒定的執(zhí)行次數(shù),比如每200ms檢查下滾動位置,并觸發(fā) CSS 動畫。
  • requestAnimationFrame :可替代 throttle ,函數(shù)需要重新計(jì)算和渲染屏幕上的元素時,想保證動畫或變化的平滑性,可以用它。注意:IE9 不支持。

產(chǎn)考:http://outofmemory.cn/javascript/js-debouce-throttle

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容