输入框搜索建议、滚动事件监听、窗口resize……这些场景如果直接绑定事件处理函数,会频繁触发回调,性能直接拉胯。
解决方案就两个:防抖(debounce)和节流(throttle)。今天手写实现,彻底搞懂。
问题场景
// 搜索框实时联想
input.addEventListener('input', function(e) {
fetchSuggestions(e.target.value);
// 用户输入"hello",触发了5次请求
// h → he → hel → hell → hello
});
// 滚动事件
window.addEventListener('scroll', function() {
updatePosition();
// 滚动一屏可能触发几百次
});
防抖(debounce)
防抖的思路:事件触发后等N毫秒,如果期间没有再次触发,才执行。如果期间又触发了,重新计时。
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 每次触发都清除上一个定时器
if (timer) clearTimeout(timer);
// 重新设一个定时器
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 使用
const handleSearch = debounce(function(value) {
fetchSuggestions(value);
}, 300);
input.addEventListener('input', function(e) {
handleSearch(e.target.value);
});
工作原理:
用户输入:h e l l o
触发时间:0 100 200 300 400
debounce(300ms):
0ms: 设定时器,300ms后执行
100ms: 清除上一个,重新设
200ms: 清除上一个,重新设
300ms: 清除上一个,重新设
400ms: 清除上一个,重新设
700ms: 定时器到期,执行!
结果:只执行了1次
节流(throttle)
节流的思路:规定一个时间间隔内,最多只执行一次。
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用
const handleScroll = throttle(function() {
updatePosition();
}, 200);
window.addEventListener('scroll', handleScroll);
工作原理:
滚动事件:0 50 100 150 200 250 300 350 400
throttle(200ms):
0ms: 执行!(距上次0ms ≥ 200ms?不,lastTime初始为0,now-0 ≥ 200,执行)
50ms: 不执行(距上次50ms < 200ms)
100ms: 不执行
150ms: 不执行
200ms: 执行!(距上次200ms ≥ 200ms)
250ms: 不执行
300ms: 不执行
350ms: 不执行
400ms: 执行!(距上次200ms ≥ 200ms)
结果:执行了3次
防抖 vs 节流
| 对比 | 防抖(debounce) | 节流(throttle) |
|---|---|---|
| 核心思想 | 等停下来再执行 | 固定频率执行 |
| 执行频率 | 停止触发后执行1次 | 每N毫秒最多执行1次 |
| 适用场景 | 搜索联想、表单验证 | 滚动监听、resize |
| 响应速度 | 慢(要等延迟) | 快(固定间隔) |
进阶版:带立即执行的防抖
有时候希望第一次就执行,后面再防抖:
function debounceImmediate(fn, delay) {
let timer = null;
return function(...args) {
if (timer === null) {
// 第一次立即执行
fn.apply(this, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
}, delay);
};
}
进阶版:带尾调用的节流
标准节流可能在持续触发时"饿死"——如果间隔内一直触发,最后一次可能丢失。加上尾调用保证最后一次也能执行:
function throttleWithTail(fn, interval) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
// 设置尾调用
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
lodash的用法
实际项目中直接用lodash就好:
import { debounce, throttle } from 'lodash';
// 防抖
const handleSearch = debounce(fetchSuggestions, 300);
// 节流
const handleScroll = throttle(updatePosition, 200);
// 在Vue中使用
export default {
methods: {
handleSearch: debounce(function() {
this.fetchData();
}, 300)
},
beforeDestroy() {
// 组件销毁时取消
this.handleSearch.cancel();
}
}
防抖和节流是前端性能优化的基本功,面试也常考手写。理解了原理,手写出来不难,关键是要分清楚什么场景用哪个。
你们项目中哪些地方用到了防抖节流?