防抖(Debounce)与节流(Throttle):区别、实现与应用场景
面试速答(30 秒版 TL;DR)
- 防抖:多次触发,只在“停止触发一段时间后”执行一次;适合输入搜索、窗口 resize 结束后计算。
- 节流:持续触发时,保证“每隔一段时间最多执行一次”;适合滚动监听、拖拽、鼠标移动。
- 一句话区分:防抖是“只要还在抖,就不执行”;节流是“你继续抖也行,但我按频率执行”。
- 实现本质:防抖依赖“重置定时器”,节流依赖“时间窗口 + 锁”。
心智模型
假设用户在 1 秒内连续点击 10 次:
- 防抖:前 9 次都取消,最后停下来才执行 1 次。
- 节流:这一秒里可能按固定频率执行 2 到 3 次,而不是 10 次。
适用场景可以这么背:
| 场景 | 适合什么 | 原因 |
|---|---|---|
| 搜索框联想 | 防抖 | 用户停止输入后再请求,减少无效接口调用 |
| 窗口 resize | 防抖 | 关心最终布局结果 |
| 页面滚动统计 | 节流 | 持续触发时要定期上报 |
| 拖拽 / 鼠标移动 | 节流 | 需要持续反馈,但不能每次事件都重算 |
最小实现
防抖
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
原理:
- 每次触发都先清掉旧定时器;
- 重新开启一个新的定时器;
- 只有最后一次触发后的等待时间完整走完,才真正执行。
节流
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime < interval) return;
lastTime = now;
fn.apply(this, args);
};
}
原理:
- 记录上一次执行时间;
- 当前时间和上次时间差不够,就直接跳过;
- 只有进入下一个时间窗口才执行。
进阶实现:立即执行、尾执行、取消
面试官如果继续追问,通常是这几个点:
- 防抖能不能第一次立刻执行?
- 节流能不能在最后补一次?
- 已经注册的防抖 / 节流任务能不能取消?
可立即执行的防抖
function debounce(fn, delay, immediate = false) {
let timer = null;
function debounced(...args) {
const callNow = immediate && timer === null;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
}
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
带尾执行的节流
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
function throttled(...args) {
const now = Date.now();
const remain = interval - (now - lastTime);
if (remain <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
fn.apply(this, args);
return;
}
if (timer) return;
timer = setTimeout(() => {
timer = null;
lastTime = Date.now();
fn.apply(this, args);
}, remain);
}
throttled.cancel = () => {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}
和事件循环的关系
防抖 / 节流通常基于 setTimeout,所以它们本质上依赖浏览器或 Node 的定时器调度能力。
注意两个边界:
setTimeout(fn, 16)不是精确 16ms 后执行,只是“最早不早于 16ms”。- 如果主线程很忙,真正执行时间会更晚。
因此面试时可以补一句:
- 防抖 / 节流解决的是“高频触发下的调用控制”,不是“精确调度”。
requestAnimationFrame 节流
如果场景与渲染强相关,比如滚动联动、元素跟手动画,可以用 requestAnimationFrame 做“按帧节流”:
function rafThrottle(fn) {
let locked = false;
return function (...args) {
if (locked) return;
locked = true;
requestAnimationFrame(() => {
locked = false;
fn.apply(this, args);
});
};
}
优点:
- 更贴近浏览器刷新节奏;
- 比固定毫秒节流更适合视觉更新。
高频题标准答法
1. 防抖和节流的区别是什么
防抖关注“最后一次”,节流关注“执行频率上限”。
2. 搜索框为什么更适合防抖
因为用户输入过程中大多是中间态,没必要每次都发请求,等停下来再发更合理。
3. 滚动监听为什么更适合节流
因为滚动过程中需要持续反馈,不能等滚动完全停下才更新。
4. 节流一定比防抖好吗
不是。两者是不同业务语义,没有绝对优劣,关键看你关心“最后一次”还是“过程中的稳定频率”。
易错点 / 坑
- 忘记保留
this和参数,导致作为对象方法或事件处理函数时行为异常。 - 以为防抖 / 节流能解决性能问题的根源;它们只能减少调用次数,真正耗时逻辑仍需要优化。
- 节流只做“首执行”不做“尾执行”,导致最后一次状态丢失。
- 组件卸载时不取消定时器,可能引发内存泄漏或操作已销毁实例。
速记要点(可背诵)
- 防抖:重置定时器,停下来再执行。
- 节流:限制频率,一个时间窗口最多执行一次。
- 搜索框用防抖,滚动拖拽用节流。
- 视觉更新优先考虑
requestAnimationFrame节流。