防抖与节流:从手写实现到边界条件
搜索框输入、窗口缩放、滚动加载都会高频触发回调。防抖(debounce)与节流(throttle)是两种不同的收敛策略,选错会让交互变味。
语义差异
- 防抖:事件停止触发一段时间后才执行。适合「等用户停下来」的场景,如搜索联想、表单校验。
- 节流:固定时间内最多执行一次。适合「过程中要持续反馈」的场景,如滚动进度、拖拽。
防抖的实现
function debounce(fn, wait = 300, immediate = false) {
let timer = null;
return function (...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, wait);
if (callNow) fn.apply(this, args);
};
}
注意 this 与参数要通过 apply 透传,否则在类方法或事件处理器里会丢上下文。
节流的实现
function throttle(fn, wait = 200) {
let last = 0, timer = null;
return function (...args) {
const now = Date.now();
const remain = wait - (now - last);
if (remain <= 0) {
last = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, args);
}, remain);
}
};
}
这个版本保证最后一次触发不会丢,比简单的「时间戳判断」更符合预期。
三个常见错误
一、在渲染函数里创建防抖函数。每次渲染都生成新实例,防抖完全失效。要在组件外或用 useRef 保存。
二、卸载时没有清理定时器。组件销毁后回调仍会执行,可能访问已释放的 DOM。
三、用防抖处理滚动事件。用户滚动期间毫无反馈,体验反而更差——这种场景应该用节流或 requestAnimationFrame。
什么时候不用
如果事件本身频率不高(比如点击提交),加防抖只会增加延迟,还可能导致用户以为没点上。判断依据是:回调的实际开销是否值得用延迟换频率。