作者:Trey Huffine
翻譯:瘋狂的技術宅
原文:levelup.gitconnected.com/throttle-in…
未經允許嚴禁轉載
調節器是瀏覽器中通過限制程式碼要處理的事件數量來提高效能的常用技術。當你想以受控的速率執行回撥時,應該使用調節器,它允許你在每個固定的時間間隔內重複處理過渡狀態。
我將以一個真實世界的類比開始,然後在 Web 上下文中描述調節器,最後提供有關如何實現節流的註釋程式碼示例。在文章的結尾,有一個帶有調節器示例的 Codepen,你可以與之互動以檢視其工作原理。如果只關心程式碼,請跳至 “JavaScript 中的調節器實現” 部分。
調節器是“去抖動” 的表親,它們都可以提高 Web 應用的效能。但是它們在不同的情況下使用。當你只關心最終狀態時,會使用去抖功能。例如等待使用者停止鍵入以獲取預先輸入的搜尋結果。當你想要以受控的速率處理所有中間狀態時,最好使用調節器。例如,當使用者調整視窗大小並在頁面內容改變時重新排列頁面內容時跟蹤螢幕寬度,而不是等到使用者完成操作時再跟蹤。
真實世界中調節器的例子
一個比喻是我們的飲食方式。我們想節制飲食,以便每 6 小時吃一頓飯。我們早上 7 點起床吃早餐,然後節流,直到下午 1 點吃午餐,最後在晚上 7 點吃晚餐。每次吃完飯後,我們就會阻止自己進食 6 個小時,以確保整天都能以合理的增量獲得食物。
這種類比可以擴充套件到生活中以設定的增量去執行動作的任何情形。例如,我們希望每三個月更換一次汽車中的機油。我們不會提前這樣做,因為那是在浪費金錢,我們也不會拖延,因為這會損壞汽車引擎。我們會檢查擋風玻璃上的貼紙,看是否經過了足夠的時間,然後我們去找機械師。因此,我們會每 3 個月就進行一次換油,這樣可以最有效地處理換油事件。
Web 開發中的節流
為了理解 Web 開發上下文中的限制,假設你有一個滾動事件處理程式,當使用者在頁面上向下移動時,你想在其中向使用者顯示新內容。如果在每次使用者滾動單個畫素時都執行回撥,假如快速滾動的話,我們將會很快就被事件阻塞,因為它將快速連續傳送數百或數千個事件。相反,我們對其進行限制,僅每 100 毫秒檢查一次滾動,這樣每秒僅獲得10個回撥。使用者仍然可以立即感覺到響應,但是計算效率更高。
調節器用於建立均勻間隔的函式呼叫。想象一下,如果你在事件處理程式回撥函式中執行大量計算或 API 請求。通過限制這些回撥,可以防止應用凍結或對伺服器發出不必要地請求。
JavaScript 中的調節器的實現
讓我們立即進入調節器程式碼。我會在下面進行描述,然後提供該功能的註釋版本。
const throttle = (callback, delay) => {
let throttleTimeout = null;
let storedEvent = null;
const throttledEventHandler = event => {
storedEvent = event;
const shouldHandleEvent = !throttleTimeout;
if (shouldHandleEvent) {
callback(storedEvent);
storedEvent = null;
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
if (storedEvent) {
throttledEventHandler(storedEvent);
}
}, delay);
}
};
return throttledEventHandler;
};
複製程式碼
這個調節器的實現是最簡單易懂的。它僅用於教學目的,並非是效率最高或程式碼行數最少的。
調節器是一個高階函式,這是一個返回另一函式的函式(為清楚起見,此處命名為 throttledEventHandler
)。這樣做是為了圍繞 callback
、delay
、throttleTimeout
和 storedEvent
引數形成一個閉包。這保留了在執行 throttledEventHandler
時要讀取的每個變數的值。以下是每個變數的定義:
callback
:你想要以給定速率執行的節流函式。delay
:你希望節流函式在多次執行callback
之間等待的時間。throttleTimeout
: The value used to indicate a running throttle created by oursetTimeout
.throttleTimeout
:該值用於指示由setTimeout
建立的調節器。storedEvent
:你想通過節流callback
處理的事件。該值將不斷更新,直到截流結束。
我們可以在以下程式碼中使用調節器:
var returnedFunction = throttle(function() {
// Do all the taxing stuff and API requests
}, 500);
window.addEventListener('scroll', returnedFunction);
複製程式碼
由於調節器返回一個函式,因此第一個例子中的 throttledEventHandler
和第二個例子中的 returnedFunction
函式實際上是相同的函式。每次使用者滾動滑鼠時,它將執行 throttledEventHandler
/returnedFunction
。
下面逐步說明在截流函式時會發生什麼。首先,我們圍繞變數建立一個閉包,以便每次執行時它們都可用於ThrottledEventHandler
。 ThrottledEventHandler
接收到 1 個作為事件的引數。它將事件儲存在 storedEvent
變數中。
然後檢查執行是否超時(即啟用調節器)。如果調節器生效,那麼 throttledEventHandler
已經完成了該執行並等待執行回撥。如果調節器為非活動狀態,則可以用回撥函式立即處理該事件。然後呼叫 setTimeout
並儲存超時值,該值表明調節器正在生效。
當 timeout 處於活動狀態時,將始終儲存最新事件。這時則會跳過回撥的執行,這可以使我們免於執行 CPU 密集型任務或呼叫我們的 API。
當 setTimeout
結束時,將 throttleTimeout
置為空,這表明該函式不再受到限制並且可以處理事件。如果有一個 storedEvent
,我們想立即處理它,這是則會遞迴地呼叫 throttledEventHandler
。 setTimeout
內部的遞迴呼叫使我們能夠以恆定的速率處理事件。只要有新事件繼續發生,它就會在期望的延遲後重復執行相同的處理過程。
該函式的註釋版本:
// 傳遞我們要限制的回撥以及限制事件之間的延遲
const throttle = (callback, delay) => {
// 在這些變數周圍建立一個閉包。
// 它們將在調節器處理的所有事件之間共享。
let throttleTimeout = null;
let storedEvent = null;
// 當調節器處於活動狀態時,此函式將處理事件和調節器回撥。
const throttledEventHandler = event => {
// 每次迭代都更新儲存的事件
storedEvent = event;
// 如果調節器尚未啟用,我們將使用事件執行回撥
const shouldHandleEvent = !throttleTimeout;
// 如果沒有活動的調節器,將執行回撥並建立一個新的調節器。
if (shouldHandleEvent) {
// 處理我們的事件
callback(storedEvent);
// 由於我們使用了已儲存的事件,因此將其清空。
storedEvent = null;
// 通過設定超時來建立新的限制,以防止在延遲期間處理事件。
// 超時結束後,如果有儲存的事件,則執調節器。
throttleTimeout = setTimeout(() => {
// 由於調節器時間已到期,因此我們立即使調節器超時無效。
throttleTimeout = null;
// 如果我們有一個儲存的事件,則遞迴呼叫此函式。
// 遞迴使我們能夠在事件發生時連續執行。
// 如果事件停止了,我們的調節器將結束。 如果有新事件發生,它將立即執行。
if (storedEvent) {
// 由於超時結束:
// 1. 由於節流時間現在為 null,因此本遞迴呼叫將立即執行 `callback`
// 2. 它將重新啟動調節器 timer,使我們可以重複調節器過程
throttledEventHandler(storedEvent);
}
}, delay);
}
};
// 返回受限制的事件處理作為閉包
return throttledEventHandler;
};
複製程式碼
互動示例
總結
對於 JavaScript 開發人員而言,節流是一個非常重要且有益的概念。它是提高 Web 應用效能的常用工具,從頭開始實施節流功能還可以增強你的高階 JS 技術,例如閉包、非同步事件處理、高階函式和遞迴。