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

雯子ATHENA發表於2019-02-28

前言:這篇文章是lodash文件裡在 Debounce 和 Throttle 內容中推薦閱讀的一篇文章,所以建議想要詳細瞭解這塊技術的同學可以看一看這篇文章,有助於你直觀的理解。

DebounceThrottle 是兩個很相似但是又不同的技術,都可以控制一個函式在一段時間內執行的次數。

當我們在操作 DOM 事件的時候,為函式新增 debounce 或者 throttle 就會尤為有用。為什麼?因為我們在事件和函式執行之間加了一個我們自己的控制層。記住,我們是不去控制這些 DOM 事件觸發的頻率的,因為這個可能會有變化。

下面我們以滾動事件舉例:

See the Pen Scroll events counter by Athena (@athena0304) on CodePen.

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

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

John 當時提出的解決方案是在 onScroll event 的外部設定一個每 250ms 執行一次的迴圈。這樣處理程式就與事件解耦了。使用這樣一個簡單的技術就可以避免破壞使用者體驗。

::: tip 譯者注

文中的核心程式碼如下

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);
複製程式碼

:::

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

Debounce

Debounce 允許我們將多個連續的呼叫合併成一個。

img

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

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

See the Pen Debounce. Trailing by Athena (@athena0304) on CodePen.

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

Leading 邊緣 (或者 "immediate")

在上面的例子中,你會發現 debounce 事件會等到快速事件停止發生後才會觸發函式執行。為什麼不在每次一開始就立即觸發函式執行呢,這樣它的表現就和原始的沒有去抖的處理器一樣了。直到快速呼叫出現停頓的時候,才會再次觸發。

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

img

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

自己試一下:

See the Pen Debounce. Leading by Athena (@athena0304) on CodePen.

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 事件。

例子如下:

See the Pen Debounce Resize Event Example by Athena (@athena0304) on CodePen. 可以看到,我們在 resize 事件上使用的是預設的 `trailing` 選項,因為我們只需要關心使用者停止調整瀏覽器後的最終結果就可以了。
敲擊鍵盤,通過 Ajax 請求自動填充表單

為什麼要在使用者還在輸入的時候每隔 50ms 就傳送一次 Ajax請求?_.debounce 可以幫助我們避免額外的開銷,只有當使用者停止輸入了再傳送請求。

這裡沒有必要設定 leading,我們是想要等到最後一個字元輸入完再執行函式的。

See the Pen Debouncing keystrokes Example by Athena (@athena0304) on CodePen. 還有一個類似的使用場景就是表單校驗,當使用者輸入完再進行校驗、提示資訊等。

如何使用 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, 但是 IE9、Opera Mini 和一些老的安卓版本還不支援。如果需要,現在還是要使用 polyfill
  • Node.js 不支援 rAF,所以不能在服務端用於 throttle 檔案系統事件。

根據經驗,如果你的 JavaScript 函式是在繪製或者直接改變屬性,所有涉及到元素位置重新計算的,我會建議使用 requestAnimationFrame

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

這時你可能會想,為什不把 rAF 整合到 underscore 或 lodash 裡呢,那他倆都是拒絕的,因為這只是一個特殊的使用場景,而且已經足夠簡單,可以被直接呼叫。

rAF 舉例

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

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

See the Pen Scroll comparison requestAnimationFrame vs throttle by Corbacho (@dcorb) on CodePen.

還有一個更高階的例子,在 headroom.js 中,邏輯被解耦了,並且包裹在了物件中。

總結

使用 debounce、throttle 和 requestAnimationFrame 來優化你的事件處理程式。每種技術都有些許的不同,但是三個都是很有用的,而且能夠互補。

總結:

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

原文連結:css-tricks.com/debouncing-…

更多前端內容請關注下方公眾號,您的一點鼓勵就是我極大的動力,希望和大家共同學習:

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

相關文章