【譯】經過例子解釋 Debounce 和 Throttle

DebounceThrottle 是兩個很類似可是又不一樣的技術,均可以控制一個函數在一段時間內執行的次數。javascript

當咱們在操做 DOM 事件的時候,爲函數添加 debounce 或者 throttle 就會尤其有用。爲何?由於咱們在事件和函數執行之間加了一個咱們本身的控制層。記住,咱們是不去控制這些 DOM 事件觸發的頻率的,由於這個可能會有變化。css

下面咱們以滾動事件舉例:html

<iframe height='265' scrolling='no' title='Scroll events counter' src='//codepen.io/athena0304/embed/Yjbqar/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Scroll events counter.
</iframe>html5

當使用觸控板、鼠標滾輪,或者直接拽動滾動條,每秒均可以輕易觸發至少30次事件,並且在觸屏的移動端,甚至會達到每秒100次,面對這樣高的執行頻率,你的滾動事件處理程序可否很好地應對?java

在2011年,Twitter 網站提出了一個 issue:當向下滾動 Twitter 信息流的時候,整個頁面的響應速度都會變慢。 John Resig 基於該問題發表了一篇博客,文中指出,直接在 scroll 事件裏掛載一些計算量大的函數是件多麼不明智的行爲。jquery

John 當時提出的解決方案是在 onScroll event 的外部設置一個每 250ms 執行一次的循環。這樣處理程序就與事件解耦了。使用這樣一個簡單的技術就能夠避免破壞用戶體驗。webpack

::: tip 譯者注git

文中的核心代碼以下github

var outerPane = $details.find(".details-pane-outer"),
    didScroll = false;

$(window).scroll(function() {
    didScroll = true;
});

setInterval(function() {
    if ( didScroll ) {
        didScroll = false;
        // Check your page position and then
        // Load in more results
    }
}, 250);

:::web

現在,處理事件的方式稍微複雜了一些。下面咱們結合用例,一一介紹 Debounce、 Throttle 和requestAnimationFrame。

Debounce

Debounce 容許咱們將多個連續的調用合併成一個。

img

想象一個進電梯的場景,你走進了電梯,門剛要關上,這時另外一我的想要進來,因而電梯沒有移動樓層(處理函數),而是將門打開讓那我的進來。這時又有一我的要進來,就又會上演剛纔那一幕。也就是說,電梯延遲了它的函數(移動樓層)執行,可是優化了資源。

在下面的例子中,嘗試快速點擊按鈕或者在上面滑動:

<iframe height='265' scrolling='no' title='Debounce. Trailing' src='//codepen.io/athena0304/embed/NBVjRB/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce. Trailing.
</iframe>

你能夠看到連續快速事件是怎樣被一個單獨的 debounce 事件所替代的。可是若是事件觸發時間間隔較長,就不會發生 debounce。

Leading 邊緣 (或者 "immediate")

在上面的例子中,你會發現 debounce 事件會等到快速事件中止發生後纔會觸發函數執行。爲何不在每次一開始就當即觸發函數執行呢,這樣它的表現就和原始的沒有去抖的處理器同樣了。直到快速調用出現停頓的時候,纔會再次觸發。

下面是使用 leading 標識符的例子:

img

在 underscore.js 中,該選項叫做 immediate ,而不是 leading

本身試一下:

<iframe height='265' scrolling='no' title='Debounce. Leading' src='//codepen.io/athena0304/embed/mGbgGo/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce. Leading.
</iframe>

Debounce 的實現

Debounce 的概念和實現最先是由 John Hann 在2009年提出來的。

不久以後,Ben Alman 就寫了一個 jQuery 插件(如今已經再也不維護了),一年以後 Jeremy Ashkenas 把它添加進了 underscore.js。再後來被添加進 Lodash。

這三個實如今內部有一點不一樣,可是接口幾乎是相同的。

曾經有一段時間,underscore 採起了 Lodash 裏面的 debounce/throttle 實現,可是後來我在2013年發現了 _.debounce 函數的一個 bug。從那時起,這兩種實現就出現分化了。

Lodash 爲 _.debounce_.throttle 添加了更多的特性。最初的 immediate 標識符被 leadingtrailing 所替代。你能夠選擇一個選項,也能夠兩個都要。默認狀況下 trailing 是被開啓的。

新的 maxWait 選項(目前只存在於Lodash)在本文中沒有說起,可是它也是一個頗有用的選項。實際上,throttle 函數就是使用 _.debounce 帶着 maxWait 的選項來定義的,你能夠在這裏查看源碼

Debounce 舉例

Resize 舉例

經過拖拽瀏覽器窗口,能夠觸發不少次 resize 事件。

例子以下:

<iframe height='265' scrolling='no' title='Debounce Resize Event Example' src='//codepen.io/athena0304/embed/KxPLZy/?height=265&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce Resize Event Example.
</iframe>
能夠看到,咱們在 resize 事件上使用的是默認的 trailing 選項,由於咱們只須要關心用戶中止調整瀏覽器後的最終結果就能夠了。

敲擊鍵盤,經過 Ajax 請求自動填充表單

爲何要在用戶還在輸入的時候每隔 50ms 就發送一次 Ajax請求?_.debounce 能夠幫助咱們避免額外的開銷,只有當用戶中止輸入了再發送請求。

這裏沒有必要設置 leading,咱們是想要等到最後一個字符輸入完再執行函數的。

<iframe height='265' scrolling='no' title='Debouncing keystrokes Example' src='//codepen.io/athena0304/embed/NLKVZw/?height=265&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debouncing keystrokes Example.
</iframe>
還有一個相似的使用場景就是表單校驗,當用戶輸入完再進行校驗、提示信息等。

如何使用 debounce 和 throttle,以及常見問題

說了這麼多,你可能已經想本身來寫 debounce/throttle 函數了,或者是從網上隨便一篇博客上拷貝一份下來。可是我給你的建議是直接使用 underscore 或者 Lodash。 若是你只是須要 _.debounce_.throttle 函數,可使用 Lodash custom builder 來輸出一個自定義的壓縮後爲 2KB 的庫。可使用下列命令來進行構建:

npm i -g lodash-cli
lodash include = debounce, throttle

也就是說,最好是使用模塊化的形式,經過 webpack/browserify/rollup 來引用,如 lodash/throttlelodash/debouncelodash.throttlelodash.debounce

使用 _.debounce 函數的一個常見錯誤就是屢次調用它:

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

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

爲 debounced 函數建立一個變量可讓咱們調用私有函數 debounced_version.cancel(),若是有須要,lodash 和 underscore.js 均可以供你使用。

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

// 若是你須要的話
debounced_version.cancel();

Throttle

使用 _.throttle 則不容許函數每 X 毫秒的執行次數超過一次。

Throttle 和 debounce 最主要的區別就是 throttle 保證函數每 X 毫秒至少執行一次。

和 debounce 同樣, throttle 也用在了 Ben 的插件、underscore.js 和 lodash裏面。

Throttling 舉例

無限滾動

這是一個很是常見的例子。用戶在一個無限滾動的頁面裏向下滾動,你須要知道當前滾動的位置距離底部還有多遠,若是接近底部了,咱們就得經過 Ajax 請求獲取更多的內容,將其添加到頁面裏。

此時咱們以前的 _.debounce 就派不上做用了。使用 debounce 只有當用戶中止滾動時才能觸發,而咱們須要的是在用戶滾動到底部以前就開始獲取內容。

使用 _.throttle 就能確保實時檢查距離底部還有多遠。

requestAnimationFrame (rAF)

requestAnimationFrame 是另外一種限制函數執行速度的方法。

它能夠被看作 _.throttle(dosomething, 16)。但它有着更高的保真度,由於它是瀏覽器的原生 API,有着更好的精度。

咱們可使用 rAF API,做爲 throttle 函數的替代,考慮下面的優缺點:

優勢

  • 目標是 60fps(每幀 16ms),可是會在瀏覽器內部決定如何安排渲染的最佳時機。
  • 很是簡單,並且是標準 API,在將來也不會改變。更少的維護成本。

缺點

  • rAFs 的開始/取消由咱們本身來管理,而不像 .debounce.throttle 是在內部管理的。
  • 若是瀏覽器的 tab 頁面不活躍了,它就不會再執行。
  • 雖然全部的現代瀏覽器都提供了 rAF, 可是 IE九、Opera Mini 和一些老的安卓版本還不支持。若是須要,如今仍是要使用 polyfill
  • Node.js 不支持 rAF,因此不能在服務端用於 throttle 文件系統事件。

根據經驗,若是你的 JavaScript 函數是在繪製或者直接改變屬性,全部涉及到元素位置從新計算的,我會建議使用 requestAnimationFrame

若是是處理 Ajax 請求,或者決定是否添加/刪除某個 class(可能會觸發一個 CSS 動畫),我會考慮 _.debounce_.throttle,這裏能夠設置更低一些的執行速度(例如 200ms,而不是16ms)。

這時你可能會想,爲什不把 rAF 集成到 underscore 或 lodash 裏呢,那他倆都是拒絕的,由於這只是一個特殊的使用場景,並且已經足夠簡單,能夠被直接調用。

rAF 舉例

這篇文章的啓發,在這裏我會舉一個滾動的例子,在這篇文章中有每一個步驟的邏輯解釋。

我作了一個對比實驗,一邊是 rAF,一邊是 16ms 間隔的 _.throttle。它們性能很類似,可是 rAF 可能會在更復雜的場景下性能更高一些。

<p data-height="265" data-theme-id="0" data-slug-hash="VGKMWv" data-default-tab="js,result" data-user="athena0304" data-pen-title="Scroll comparison requestAnimationFrame vs throttle" class="codepen">See the Pen Scroll comparison requestAnimationFrame vs throttle</p>
<script async src="https://static.codepen.io/ass...;></script>

還有一個更高級的例子,在 headroom.js 中,邏輯被解耦了,而且包裹在了對象中。

總結

使用 debounce、throttle 和 requestAnimationFrame 來優化你的事件處理程序。每種技術都有些許的不一樣,可是三個都是頗有用的,並且可以互補。

總結:

  • debounce:將一系列迅速觸發的事件(例如敲擊鍵盤)合併成一個單獨的事件。
  • throttle:確保一個持續的操做流以每 X 毫秒執行一次的速度執行。例如每 200ms 檢查一下滾動條的位置來觸發某個 CSS 動畫。
  • requestAnimationFrame:throttle的一個替代品。適用於須要計算元素在屏幕上的位置和渲染的時候,可以保證動畫或者變化的平滑性。注意:IE9 不支持。

原文連接:https://css-tricks.com/deboun...

相關文章
相關標籤/搜索