- Published on
高频事件优化:防抖与节流
在 Web 开发中,某些 DOM 事件(如 resize, scroll, input, mousemove)的触发频率极高,可能在短时间内被触发数百次。如果事件监听器 (event listener) 的回调函数中包含了复杂的计算或 DOM 操作,这种高频次的执行会严重阻塞 JavaScript 主线程,导致 UI 卡顿和糟糕的用户体验。为了解决这一问题,防抖 (Debounce) 和节流 (Throttle) 作为两种核心的函数率限制技术应运而生。它们通过控制回调函数的执行时机与频率,在保证必要响应的同时,极大地优化了应用性能。
节流 (Throttle):时间窗口内的“阀门”
节流的核心思想是:在指定的时间间隔内,无论事件被触发多少次,回调函数最多只执行一次。
机制: 它像一个“阀门”,设定了一个流量阈值。在第一个事件触发时,它会立即执行回调,然后关闭“阀门”。在设定的 duration 时间内,所有后续的事件触发都会被忽略。直到 duration 结束后,“阀门”才会再次打开,准备响应下一次事件。
基础实现
/**
* @param {Function} func 要节流的函数
* @param {number} duration 节流的时间间隔 (ms)
*/
function throttle(func, duration) {
// 通过闭包维持一个“是否应等待”的状态
let shouldWait = false;
return function (...args) {
if (!shouldWait) {
func.apply(this, args);
shouldWait = true;
setTimeout(() => {
shouldWait = false;
}, duration);
}
};
}
高级配置与应用
leading 与 trailing 选项成熟的 throttle 实现(如 Lodash 库)通常提供 leading (首次是否立即执行) 和 trailing (末次是否执行) 两个布尔值选项,以满足不同场景:
{ leading: true, trailing: false }(默认): 立即响应第一次操作,然后进入冷却。适用于防止重复提交的按钮点击。{ leading: false, trailing: true }: 第一次不执行,只在时间窗口的末尾响应最后一次操作。适用于需要等待操作稳定后再响应的场景,如拖拽结束后的位置计算。{ leading: true, trailing: true }: 既能立即响应,又能保证最后一次被忽略的操作得到处理。适用于需要即时反馈,又不能丢失最终状态的场景,如复杂表单的实时校验。
防抖 (Debounce):"冷静期"后的执行
防抖的核心思想是:事件被触发后,延迟 duration 时间再执行回调。如果在这 duration 时间内事件被再次触发,则重新开始计时。
机制: 它总是在等待一个“冷静期”。只有当事件停止触发,并经过一个完整的 duration 后,回调函数才会被真正执行。它关注的是最后一次操作。
基础实现
/**
* @param {Function} func 要防抖的函数
* @param {number} duration 防抖的延迟时间 (ms)
*/
function debounce(func, duration) {
// 通过闭包维持一个定时器 ID
let timeout;
return function (...args) {
// 如果定时器已存在,则清除它,实现“重新计时”
clearTimeout(timeout);
// 设置一个新的定时器
timeout = setTimeout(() => {
func.apply(this, args);
}, duration);
};
}
典型应用场景
- 搜索框输入 (
input事件): 仅在用户停止输入后,才发起 API 请求。 - 窗口
resize事件: 仅在用户完成窗口尺寸调整后,才重新计算布局。 - 自动保存: 仅在用户停止编辑一段时间后,才触发保存操作。
实践中的关键陷阱
debounce 和 throttle 的核心在于利用闭包 (Closure) 来维持其内部状态(shouldWait 或 timeout)。如果在事件监听器的回调函数内部才去调用 debounce 或 throttle,那么每一次事件触发,都会创建一个全新的、状态独立的防抖/节流函数,从而使其完全失效。
错误示例:
// ❌ 每次点击,都会创建一个新的 debounce 实例,timeout 永远无法被共享和清除
button.addEventListener('click', function handleButtonClick() {
const debouncedFunc = debounce(() => console.log('Clicked!'), 500);
debouncedFunc();
});
正确实践:
// ✅ 在 addEventListener 之前,就创建好唯一的 debounce 实例
const debouncedClickHandler = debounce(function handleButtonClick() {
console.log('Clicked!');
}, 500);
button.addEventListener('click', debouncedClickHandler);