昨天談的是演算法本身的時間複雜度,但前端效能問題還有另一個常見來源:使用者操作觸發事件的「頻率」。打字、捲動、拖曳視窗,這些動作短短一秒內可能觸發幾十次事件,如果每一次都執行一次完整的運算或 API 呼叫,再快的演算法也會被拖垮。今天要談的防抖與節流,就是控制「事件觸發頻率」的兩種機制。
定義:在事件停止觸發「一段時間」之後,才真正執行對應的函式;如果在這段等待時間內事件又被觸發,就重新計時。效果是:只有當使用者真的停下來之後,函式才會被執行一次。
定義:規定函式在「固定時間間隔」內最多只能執行一次,不管事件在這段時間內觸發了多少次。效果是:函式會以穩定的頻率持續被執行,而不是完全等到使用者停下來。
Debounce 適合「只關心使用者『最終』的那個狀態」的場景,例如打字搜尋——使用者還在打字時不需要查詢,等他打完才查一次。Throttle 則適合「過程中也需要持續反應」的場景,例如捲動視窗時持續載入更多內容——不需要每一個捲動事件都觸發,但也不能等到使用者完全停止捲動才反應。
商品搜尋框很適合用 Debounce:使用者每打一個字都呼叫一次 API 查詢,既浪費後端資源,也容易因為回應順序不一致而顯示錯誤的結果。
// utils/debounce.js
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer); // 事件再次觸發,重新計時
timer = setTimeout(() => fn(...args), delay);
};
}
// 商品搜尋框:使用者停止打字 300ms 後,才真正呼叫搜尋 API
const handleSearch = debounce((keyword) => {
searchProducts(keyword); // 呼叫 API
}, 300);
searchInput.addEventListener('input', (e) => handleSearch(e.target.value));
商品列表捲動載入更多資料,則適合用 Throttle:即使使用者持續快速捲動,也只需要每隔一段固定時間檢查一次是否該載入下一頁,不需要每一個 scroll 事件都檢查。
// utils/throttle.js
function throttle(fn, interval) {
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime >= interval) { // 固定時間間隔內最多執行一次
lastTime = now;
fn(...args);
}
};
}
// 商品列表捲動:每 200ms 最多檢查一次是否捲到底部,需要載入下一頁
const handleScroll = throttle(() => {
if (isNearBottom()) loadMoreProducts();
}, 200);
window.addEventListener('scroll', handleScroll);
防抖讓函式「等使用者停下來才執行一次」,節流讓函式「以固定頻率持續執行」——兩者都是在不改變功能本身的前提下,控制事件被處理的頻率,跟 Day 23 談的演算法複雜度是兩個不同層次的效能問題:一個管「每次執行要花多少運算」,一個管「要執行幾次」,實務上常常需要同時處理。