防抖和節流是前端優化的基礎工具,幾乎所有項目都會用到,面試也必問。這篇文章講清楚兩者的區別和適用場景。
問題背景
有些事件觸發頻率極高:
scroll:每次滾動可能觸發幾十次resize:窗口大小變化時連續觸發input:用户每打一個字觸發一次mousemove:鼠標移動時每幀觸發
如果每次觸發都執行回調(尤其是網絡請求、DOM 操作),會導致性能問題。
節流(Throttle)
定義:在指定時間內,無論觸發多少次,只執行一次。
比喻:水龍頭限流,每分鐘最多出水一次,無論你開多大。
javascript
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用:滾動事件最多每 200ms 觸發一次
window.addEventListener(
"scroll",
throttle(() => {
console.log("scroll position:", window.scrollY);
}, 200),
);
時間戳版本每次都在間隔開始時執行(不會等到最後一次觸發)。
定時器版本(在間隔結束時執行):
javascript
function throttle(fn, delay) {
let timer = null;
return function (...args) {
if (timer) return; // 還在等待中,忽略
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
適用場景:
- 滾動加載(每 300ms 檢查一次是否到底部)
- 按鈕防重複點擊(3秒內只觸發一次)
- 鼠標跟隨動畫
- API 輪詢頻率控制
防抖(Debounce)
定義:事件停止觸發後等待指定時間,才執行回調。如果在等待期間再次觸發,重新計時。
比喻:電梯關門。有人進來就重新等,一段時間沒人進才關門。
javascript
function debounce(fn, delay) {
let timer = null;
return function (...args) {
// 清除上一次的定時器
if (timer) clearTimeout(timer);
// 重新計時
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用:輸入停止 500ms 後才搜索
const searchInput = document.getElementById("search");
searchInput.addEventListener(
"input",
debounce((e) => {
fetchSearchResults(e.target.value);
}, 500),
);
立即執行版本(第一次觸發立即執行,停止後冷卻):
javascript
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
const callNow = immediate && !timer;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
}
適用場景:
- 搜索框實時搜索(停止輸入後才請求)
- 表單驗證(停止輸入後才校驗)
- 窗口 resize 結束後重新計算佈局
- 編輯器內容變化自動保存
兩者對比
| | 節流 | 防抖 | | -------- | ------------ | -------------- | | 執行時機 | 固定間隔執行 | 停止觸發後執行 | | 適用場景 | 需要持續響應 | 等待操作完成 | | 舉例 | 滾動位置更新 | 搜索框聯想 |
核心區別:節流關心"執行頻率",防抖關心"操作是否完成"。
在 Vue 中使用
vue
<script>
import { debounce, throttle } from "lodash";
export default {
data() {
return { searchQuery: "" };
},
created() {
// 在 created 裏創建,保證每個組件實例有獨立的 debounce 函數
this.debouncedSearch = debounce(this.fetchResults, 500);
},
beforeDestroy() {
// 組件銷燬時取消等待中的調用
this.debouncedSearch.cancel();
},
methods: {
onInput(value) {
this.searchQuery = value;
this.debouncedSearch(value);
},
async fetchResults(query) {
const results = await searchAPI(query);
this.results = results;
},
},
};
</script>
注意:不要在 methods 裏直接用 debounce() 包裝,這會導致所有組件實例共享同一個 debounce 函數:
javascript
// ❌ 錯誤:methods 裏的函數是共享的
methods: {
onInput: debounce(function(value) { ... }, 500)
}
// ✅ 正確:在 created 裏創建,每個實例獨立
created() {
this.debouncedFn = debounce(this.fn, 500)
}
小結
- 高頻事件必須節流或防抖,否則有性能問題
- 節流 = 固定頻率執行(適合持續響應的場景)
- 防抖 = 等停止後執行(適合等待操作完成的場景)
- Vue 中在
created裏創建,在beforeDestroy裏 cancel