1. 程式人生 > >節流(Throttling)和去抖(Debouncing)詳解

節流(Throttling)和去抖(Debouncing)詳解

系列 ram tac upa idt 安卓 BE animation chm

這篇文章的作者是 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屬性也被替換成了leadingtrailing。默認情況下,只有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()來關閉這個”去抖”,在lodashunderscore.js中,你可以這樣用。

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

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

節流

使用_.throttle方法後會限制一個不得不需要一直執行的函數的執行頻率,比如限制它每x毫秒執行一次。
其實,”節流”和”去抖”的最大區別就是,”節流”會保證函數一直在有規律的執行(至少每x毫秒一次的頻率進行執行)。
debounce一樣,throttle方法也集成在了underscore.jslodash當中。

‘節流’實例

‘無限滾動’實例

這是一個很常見的例子,就是當用戶將頁面滾動到將近底部時,發送一個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,throttlerequestAnimationFrame去優化你的函數執行。每種方法都有一些細微的差別,但它們都非常有用。沒有最好用的方法,只有更好用的方法。

    • 去抖-debounce:把一個一連串的事件函數(例如鍵盤輸入)”打包”成一個進行執行
    • 節流-throttle:保證你的函數在一定頻率下一直執行。例如當你滾動頁面時,它會保證在每200ms檢測一下你滾動的位置。
    • requestAnimationFrame: 可以看做是`throttle`的替代品。當你的函數有很多的動畫渲染或者有很多的元素操作時,你想保證動畫的流暢性,就需要用到這個。註意:IE9不支持rAF.

節流(Throttling)和去抖(Debouncing)詳解