防抖与节流:差别只在「谁决定何时执行」
两者都是限流,区别是防抖由最后一次调用决定执行时刻,节流由固定时间窗决定。选错的典型症状是拖拽跟手但松手才更新,或者输入框该等却每 200ms 就发一次。
防抖和节流常被当成「两个相似的限流函数」来记,其实决定它们行为的是同一个问题:执行时刻由谁决定。
- 防抖(debounce):由最后一次调用决定。每次新调用都重置计时器,只有安静够久才真正执行。
- 节流(throttle):由固定的时间窗决定。一个窗口内最多执行一次。
防抖:等安静下来
function debounce<T extends unknown[]>(fn: (...a: T) => void, wait: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: T) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), wait);
};
}
搜索框输入用它:用户还在打字时不该发请求,停下来了才发。
注意它在输入过程中一次都不执行。如果用户连续打字 10 秒,这 10 秒内触发次数是零 —— 这正是要的,但也意味着「边打边更新字数」不能用它。
节流:按固定节奏执行
function throttle<T extends unknown[]>(fn: (...a: T) => void, interval: number) {
let last = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: T) => {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
clearTimeout(timer);
last = now;
fn(...args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = undefined;
fn(...args);
}, remaining);
}
};
}
滚动进度、拖拽跟随用它:要持续反馈,但不必处理每一个事件。
多出来的 else if (!timer) 分支是关键:只写 if (remaining <= 0) 的话,最后一次事件会被丢掉,拖拽松手后位置停在半路。
怎么选
| 需求 | 选择 | 理由 |
|---|---|---|
| 搜索联想 | 防抖 | 输入过程中不该发请求 |
| 窗口 resize 重排 | 防抖 | 停止拖拽后重排一次就够 |
| 滚动进度条 | 节流 | 需要持续反馈 |
| 拖拽跟随 | 节流 | 松手前每一刻都要跟 |
| 自动保存 | 防抖 | 停止编辑才存 |
判断方法只有一句:问**「中间的每一次调用,用户能不能感知到」**。感知不到就可以丢,用防抖;感知得到就必须执行,用节流。
两个常见误解
防抖一定比节流省。 不一定。持续输入时防抖确实一次都不跑,更省;但节流保证了稳定的执行频率,对手势中途要刷新状态的场景反而更合适。
requestAnimationFrame 可以替代节流。 rAF 只在页面可见时触发,且频率跟随刷新率(60Hz 与 120Hz 不同)。做纯视觉的滚动跟随它更合适,因为它与绘制同步、不会撕裂;但它不提供「至少每 N 毫秒执行一次」这个保证。
防抖管的是安静,节流管的是节奏。先想清楚要哪一个,两个函数只差三行。

评论
…