節流(Throttling)和去抖(Debouncing)詳解
這篇文章的作者是 David Corbacho,倫敦的一名前端開發工程師。之前我們有一篇關於”節流”和”去抖”的文章:The Difference Between Throttling and Debouncing(譯文:節流(Throttling)和去抖(Debouncing)的區別),但是David的這篇文章通過一些可交互的Demo來給我們做了一個更詳細的解釋。
“節流”和”去抖”都是用來限制一些需要一直執行的函數的技術,它們雖然很相似,但它們是不一樣的。
當我們需要做一些DOM事件綁定時 ,”節流”和”去抖”是非常有用的。因為這樣的話,我們在事件和事件函數之間又多了一層控制。所以,我們控制的不是事件的觸發(事件觸發的時機和頻率),而是事件觸發後去限制事件函數的執行。
例如我們非常常用的滾動事件:
當我們在電腦上進行滾動(用觸控板、鼠標滾輪、拉動滾動條)的話,事件的觸發可能會比較好控制一些,比如我想讓它每秒觸發30次,那麽我滾動的慢一點,是可以達到的。但是,如果我在移動設備上(比如智能手機,iPad等設備)上進行滑動的話,每秒鐘會觸發100次左右。這很顯然不是我們想要的結果,因為事件函數執行了太多次了。
在2011年,Twitter的網站出現過一個問題:當你滾動Twitter網站到底部進行加載數據時,頁面將會變的非常卡,有時甚至未響應。John Resig寫了一篇Learning from Twitter來說明了這個問題。
John當年(2011年)給出的解決方案是在滾動事件的外面以250毫秒為單位進行循環執行函數,這樣函數和事件之間就實現了松耦合。用了這個方法,至少就不會影響到正常的用戶體驗了。
而現在,我們有一些更好更精細的方法來處理這個問題。下面,就讓我們來介紹一下”去抖”、”節流”和”requestAnimationFrame()
“方法。我們將通過對應的案例來進行詳細的說明。
去抖
“去抖”可以讓我們把一個連續的的函數調用”打包”成一個。
舉個例子,假如你正在坐電梯,電梯門即將關上,這時突然有一個人想上電梯,於是電梯的門又開了。隨後又來了一個人,同樣的事又發生了一遍。電梯的根本功能是將你帶到其它的樓層,而現在它由於很多人按電梯,所以到其它樓層這件事兒就被延誤了,但是這樣設計的原因是為了最終能節省資源。
你可以通過下面的例子來感受一下,點擊下面的按鈕或在上面進移動:
從上面的Demo中可以看到,”去抖”是如何工作的,它把本來一系列的事件觸發變成了一個。但同時你也會發現,當我們的事件一直被觸發時(例如上面的例子當中,用鼠標不停的在按鈕上滑動),”去抖”就不起作用了。
‘去抖’前沿(去抖剛開始的時候)觸發
“去抖”後函數只會在事件結束時觸發,你可能會覺得”去抖”前的那段等待時間是非常不必要的。可是如果在一開始就觸發函數那不是跟沒有”去抖”一樣了嗎?其實不然,我們可以讓函數在”去抖”的一開始就執行一次。
下面是’前沿’選項打開後的效果:
在 underscore.js
當中,可以通過把選項當中的leading
換成immediate
來實現。
(譯者:在本段的上面和下面的例子當用的並非underscore,而是lodash)
下面是用lodash打開”前沿”觸發後的效果:
‘去抖’的實現
我第一次見到在JS中實現”去抖”是John Hann在2009年寫這篇Debouncing Javascript Methods。
之後不久,Ben Alman編寫了一個jQuery插件(該插件已經很久沒更新了),一年後,Jeremy Ashkenas又將其加入了underscore.js,隨後loadsh也加入了該功能。
在上面所說的幾種實現方式中,它們代碼內部可能會有點小區別,但是使用方式都是差不多的。
以前underscore采用了和lodash一樣的實現方式,但在2013年我發現了一個BUG。從那以後,它們則各自有自己的實現方式了。
Lodash 添加了一些其它功能在它的_.debounce
和_.throttle
方法中。原來的immediate
屬性也被替換成了leading
和trailing
。默認情況下,只有trailing
是開啟狀態。
新的maxWait
屬性(目前只在Lodash當中有)在本文章當中沒有說到,但它是一個非常有用的選項。實際上,throttle
方法在內部就是調用了帶有maxWait
參數的_.debounce
來實現的,你可以去看看lodash源代碼。
“去抖”實例
縮放實例
當縮放瀏覽器窗口大小的時候,會觸發很多次resize
事件。
上面的例子中,我們針對resize
事件用了默認的選項(開啟trailing
),因為我們只關心縮放結束後的窗口大小。
“自動完成”(鍵盤按下時進行Ajax請求)實例
當用戶進行鍵盤輸入操作時就進行發送Ajax請求,這是不合理的,因為這樣可能大概50毫秒就會發送一次請求。_.debounce
方法會幫我們解決這個問題,用了該方法後,只有當我們停止輸入的時候才會向後臺發送Ajax請求。
這個例子也是不需要開啟leading
的。因為我們想要的是用戶在輸完最好一個字母時才觸發函數。
如何使用”去抖”和”節流”以及它們的一些常見問題
我建議你使用underscore
或者Lodash
。如果你需要_.debounce
和_.throttle
方法,你可以下載Lodash的自制版本,只需要一些簡單的命令即可:
npm i -g lodash-cli
lodash-cli include=debounce,throttle
有一個常見的問題就是使用_.debounce
兩次:
// WRONG
$(window).on(‘scroll‘, function() {
_.debounce(doSomething, 300);
});
// RIGHT
$(window).on(‘scroll‘, _.debounce(doSomething, 200));
如果用一個變量把”去抖”後的函數存儲下來,那麽我們可以通過調用debounced_version.cancel()
來關閉這個”去抖”,在lodash
和underscore.js
中,你可以這樣用。
var debounced_version = _.debounce(doSomething, 200);
$(window).on(‘scroll‘, debounced_version);
// 如果有需要的話
debounced_version.cancel();
節流
使用_.throttle
方法後會限制一個不得不需要一直執行的函數的執行頻率,比如限制它每x
毫秒執行一次。
其實,”節流”和”去抖”的最大區別就是,”節流”會保證函數一直在有規律的執行(至少每x
毫秒一次的頻率進行執行)。
和debounce
一樣,throttle
方法也集成在了underscore.js
和lodash
當中。
‘節流’實例
‘無限滾動’實例
這是一個很常見的例子,就是當用戶將頁面滾動到將近底部時,發送一個Ajax請求,然後返回的數據添加到後面進行顯示。
這個時候,上面所說到的_.debounce
就不適用了,如果使用_.debounce
的話,那麽它只會在用戶停止滾動時觸發。但我們需要的是當用戶快達到底部時觸發。
使用_.throttle
,我們可以保證一直監測用戶滾動到了哪裏。
requestAnimationFrame (rAF)
requestAnimationFrame
是另一種限制函數執行頻率的方法。
rAF就像_.throttle(dosomething, 16).
方法。但是它更精確,因為它是瀏覽器內置的API。
我們可以使用rAF
來替代throttle
,下面來看一下它的優點和缺點。
優點:
- rAF的刷新頻率是`60fps`(每16毫秒刷新一次),但其實在內部,這個數字是不確定的,它會在適當的時候調為到比較適當的頻率來進行渲染
- 非常簡單的API,一看就會
缺點:
- rAFs的開始和結束是需要我們自己手動完成的,不像`.debounce`和`.throttle`會在內部自動進行完成
- 如果瀏覽器窗口此時是未激活狀態,它將不會執行。
- 即可一些現代瀏覽器都支持了rAF,但是IE9、Opera Mini以及一些舊的安卓瀏覽器是不支持的。
- nodeJS不支持rAF。
一般說來,rAF用在繪圖和動畫當中較多一點。如果是進行Ajax請求,或者進行添加、刪除class(並會有一些CSS動畫),那麽用_.debounce
或_.throttle
會更好。
rAF實例
下面的例子中,我將rAF和設置了參數為16ms
的_.throttle
進行了對比( Paul Lewis的這篇Leaner, Meaner, Faster Animations with requestAnimationFrame有更詳細的解釋)。
結語
總之,你可以使用debounce
,throttle
和requestAnimationFrame
去優化你的函數執行。每種方法都有一些細微的差別,但它們都非常有用。沒有最好用的方法,只有更好用的方法。
- 去抖-debounce:把一個一連串的事件函數(例如鍵盤輸入)”打包”成一個進行執行
- 節流-throttle:保證你的函數在一定頻率下一直執行。例如當你滾動頁面時,它會保證在每200ms檢測一下你滾動的位置。
- requestAnimationFrame: 可以看做是`throttle`的替代品。當你的函數有很多的動畫渲染或者有很多的元素操作時,你想保證動畫的流暢性,就需要用到這個。註意:IE9不支持rAF.
節流(Throttling)和去抖(Debouncing)詳解