問題的引出
在一些場景往往由于事件頻繁被觸發(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 方法是用 _.debounce加 maxWait實(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(),lodash和underscore.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