這次要介紹的是 JS 實作 Debounce 和 Throttle,它們都有助於 JS 執行上效能的優化,怎麼說呢?
由於在網頁上進行滑鼠移動 (mousemove event) 或上下滾動視窗 (scroll event),又或是視窗縮放時,會觸發多次的 DOM 監聽事件,但其實也有可能只需要經過反覆調整後的最終畫面,那這樣短時間內觸發過多次的事件將會造成效能上的不佳,因此就需要 Debounce 和 Throttle 減少事件的觸發。
透過 Debounce 處理過的函式,它會在某段時間內只執行觸發的最後一次事件,而且若事件不斷重新觸發,那就會不斷重新開始計時。
例如下圖,Debounce 設定的時間是 500 ms,若使用者瘋狂的點擊按鈕,就會不斷的重新計時,事件處理函式也就一直沒觸發,直到停下的 500 ms 後才觸發一次。
截圖的來源於 Debounce Vs Throttle: Definitive Visual Guide,有興趣的讀者也可以玩玩看。
了解 Debounce 後,我們來實作看看,加深對它的了解。
我們先將一些必要的函式加入,包括事件監聽和 Callback 函式。
function showLog(e) {
console.log(e);
console.log("hi");
}
document
.getElementById("debounceBtn")
.addEventListener("click", debounce(showLog, 1000));
debounce 會帶入兩個參數,一個就是要進行處理的函式 fn,另一個則是設定間隔多久觸發函式的時間 delay。
回傳的是一個函式,會根據設定的時間去觸發 fn 函式。
至於回傳的函式帶入參數,是為了處理 fn 所傳入的參數或是 event 物件。
function debounce(fn, delay) {
return function(...args) {
setTimeout(function() {
fn.apply(this, args);
}, delay)
}
}
接著就是完成若事件不斷重新觸發,就會重新開始計時的功能,所以加上 timeId 的判斷機制。
這裡的 setTimeout 我改成了箭頭函式,避免 this 指向 window。
function debounce(fn, delay) {
let timeId = null;
return function(...args) {
if (timeId) clearTimeout(timeId);
timeId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
不過上個步驟完成的 debounce 在第一次呼叫時不會馬上觸發,我們希望在第一次觸發事件時也執行對應的函式,故修改成如下:
function debounce(fn, delay, immediate) {
let timeId = null;
return function(...args) {
if (timeId) clearTimeout(timeId);
if (immediate && !timeId) fn.apply(this, args);
timeId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
到這邊 debounce 最主要的功能已經完成了,當然還有可以優化的地方,讀者也可以參考和閱讀 lodash 的 debounce。
LeetCode 有出現實作 Debounce 的題目,看完上面內容的讀者也可以親自試試。
透過 Throttle 處理過的函式只會在間隔指定的時間執行,像範例圖中,雖然瘋狂的點擊按鈕,但它都只會在每隔 500 ms 才觸發一次事件處理。
了解 Throttle 後,我們來實作看看,加深對它的了解。
觀念其實蠻簡單的,主要就是利用時間戳記判斷是否已經間隔超過指定的時間了,如果是就執行函式。
function throttle(fn, delay) {
let previousTime = 0;
return function(...args) {
const nowTime = new Date().getTime();
if (nowTime - previousTime > delay) {
fn.apply(this, args);
previousTime = nowTime;
}
}
}
這種方式在事件觸發時會馬上執行函式,但例如 delay 設定 1 秒,而使用者在重複點擊按鈕 3.5 秒後停止,那第 4 秒時並不會執行函式。
當還有定時器時就不會繼續執行下一個函式,直到定時器被清除才會執行。
function throttle(fn, delay) {
let timeId = null;
return function(...args) {
if (timeId) return; // 如果有計時器,表示還在 delay 的秒數內
timeId = setTimeout(() => {
timeId = null;
fn.apply(this, args);
}, delay);
}
}
不過這種方式在事件觸發時不會馬上執行函式,但例如在重複點擊按鈕 3.5 秒後停止,會在 4 秒時再執行一次事件處理函式。
讀者可以將 delay 時間拉長,反覆觀察。
如果想要在一開始馬上執行,且在最後一次事件觸發時去處理事件處理函式的話,可以這樣改寫:
function throttle(fn, delay) {
let timeId = null;
let previousTime = 0;
return function(...args) {
const nowTime = new Date().getTime();
const remain = delay - (nowTime - previousTime); // 下次觸發 fn 的時間
if (remain <= 0 || remain > delay) { // 符合執行的條件
if (timeId) timeId = null;
previousTime = nowTime;
fn.apply(this, args);
} else if (!timeId) {
timeId = setTimeout(() => {
previousTime = new Date().getTime();
timeId = null;
fn.apply(this, args);
}, remain)
}
}
}
本篇就介紹到這邊啦,這篇文章提到的範例程式在以下連結:
Debounce & Throttle JS Example
Debounce Vs Throttle: Definitive Visual Guide
函数防抖(debounce)和节流(throttle)以及lodash的debounce源码赏析
Debouncing and Throttling in JavaScript: Comprehensive Guide
Decorators and forwarding, call/apply
...
靠, 那兩張Gif好懂
以前剛碰RxJS時 也是在這兩個上有學習過
好懂又好玩(多戳幾下
真的,那兩張圖超棒Der
感謝講解這兩個重要的概念
之前面試被問到這兩個概念都會搞混
畢竟前端常碰到(連續輸入的功能就會用到)所以面試有時候會碰到
然後都是英文專有名詞的確容易搞混XD