防抖和节流有什么区别?如何实现?
简化版
防抖是事件停止触发一段时间后再执行,适合搜索输入、窗口 resize 后处理;节流是固定时间间隔内最多执行一次,适合滚动监听、拖拽、按钮高频点击。
详细版
防抖实现:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
节流实现:
function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
区别:防抖重置等待时间,强调“最后一次”;节流控制频率,强调“固定节奏”。
完整版教学
一、防抖的思路
防抖像电梯关门:有人进来就重新计时,直到一段时间没人进来才关门。搜索框联想很适合防抖,用户连续输入时不请求,停下来再请求。
核心是闭包保存 timer,每次触发先取消旧定时器,再创建新定时器。代价是响应会被推迟;如果事件永不停止,纯 trailing 防抖也可能永不执行,因此有些库还提供 maxWait 上限。
二、节流的思路
节流像水龙头限流:不管你触发多频繁,一段时间内只放一次水。滚动事件每秒可能触发几十次,如果每次都计算布局,会很卡。
节流可以用时间戳,也可以用定时器。时间戳版通常立即执行第一次;定时器版通常在间隔结束后执行。节流降低调用频率,却会丢弃或合并中间状态,所以拖拽、绘图等场景要保存最新参数,不能假设每个输入点都会被处理。
三、this 和参数不能丢
封装防抖节流时要注意保留调用时的 this 和参数:
fn.apply(this, args);
如果直接写 fn(),事件对象、组件实例上下文可能丢失。
四、工程中的增强点
成熟实现通常支持:
- leading:是否立即执行第一次。
- trailing:结束后是否补一次。
- cancel:取消等待中的执行。
- flush:立即执行等待中的函数。
面试手写基础版本即可,但能说出增强点会更像真实工程经验。
五、面试追问与工程落地
面试官可能要求实现“立即执行版防抖”。核心是在第一次触发时先执行,之后 delay 内的触发只重置定时器,等窗口期结束后才允许下一次立即执行。这类需求常见于按钮点击:第一次要立刻响应,后续防重复。
节流也常追问 leading 和 trailing。只有 leading 时,最后一次触发可能被丢掉;只有 trailing 时,第一次响应会延迟。滚动加载、拖拽预览、窗口 resize 的最佳体验往往需要根据业务选择组合。
工程里防抖节流不是万能药。输入搜索除了防抖,还要处理请求竞态:后发请求可能先返回,旧请求可能覆盖新结果。需要 abort、请求序号或只接受最后一次响应。
六、用时间线区分 leading 与 trailing
设等待窗口为 300ms,事件发生在 0ms、100ms、220ms、700ms。纯 trailing 防抖会不断重置计时器,前一组只在 520ms 执行一次,最后一次在 1000ms 执行;leading 防抖则可能在 0ms 和 700ms 立即执行。固定窗口的 leading 节流会在 0ms、700ms 执行,而带 trailing 的节流还可能在窗口末尾补上最后参数。
事件: 0 ── 100 ── 220 ───────── 700 ms
防抖: 执行520 执行1000
节流: 执行0 ───────── 可补300 ── 执行700
| 策略 | 首次响应 | 高频期间 | 停止后最后一次 | 典型场景 |
|---|---|---|---|---|
| trailing 防抖 | 延迟 | 持续重置 | 保留 | 搜索联想、校验 |
| leading 防抖 | 立即 | 忽略或重置锁 | 取决于配置 | 防重复提交 |
| leading 节流 | 立即 | 按窗口放行 | 可能丢失 | 滚动采样 |
| leading + trailing 节流 | 立即 | 限频 | 尽量保留 | 拖拽预览、进度更新 |
定时器的 delay 是最早可执行时间,不是准点保证。主线程若被一个 500ms 长任务占用,计划在 300ms 运行的 trailing 回调只能等调用栈空闲后执行,因此业务不能用防抖节流承担精确定时职责。
假设高频滚动每秒触发 120次,直接处理意味着约每 8.3ms 调用一次;用 100ms 节流后,理论上最多约每秒 10次,处理次数下降约 91.7%。若目标是与 60Hz 屏幕逐帧更新视觉效果,还可以用 requestAnimationFrame 合并同一帧内的多次输入,但它解决的是按帧调度,不等于通用节流器。
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
render(latestPosition);
});
}
先根据业务回答“第一次要不要立刻响应、停止后最后一次能不能丢”,再选择 leading/trailing;只背防抖和节流定义无法决定正确配置。
七、常见误区与追问
- 误区:防抖只是把多个事件简单延迟相同时间。 trailing 防抖会在每次触发时取消并重建等待,执行点取决于最后一次事件。
- 误区:节流能保证回调严格每隔固定毫秒执行。 定时器受主线程占用、后台页限速和宿主调度影响,只能限制频率,不能提供硬实时保证。
- 误区:加入防抖后搜索结果就不会发生竞态。 防抖减少请求数,但旧请求仍可能晚于新请求返回,需要 AbortController、序号或最新请求校验。
- 追问:为什么实现里要保存
this和args? 包装函数要保持原调用语义,并让 trailing 调用使用正确上下文与最新参数。 - 追问:
cancel应清理哪些状态? 除清除 timer 外,还应重置上次调用时间、待执行参数和上下文,避免旧窗口影响下次调用。 - 追问:节流用时间戳和定时器有什么体验差别? 时间戳版容易实现 leading,定时器版容易实现 trailing,成熟实现通常组合两者处理边界。
- 追问:页面卸载时为什么要取消等待回调? 回调继续执行可能访问已销毁组件,也会让闭包中的参数和状态被额外保留。
八、加强记忆
防抖记“停下来才执行”,节流记“按节奏执行”。输入框搜索、防重复提交常用防抖;滚动、拖拽、resize 高频监听常用节流。