防抖是“等停手再执行”,节流是“保底节奏”;防抖适用于只关心最终值的场景,节流适用于必须响应过程的场景;二者均需正确清除定时器、绑定this、独立实例化,且返回新函数须赋值使用。

防抖 debounce:不是“延迟执行”,而是“等停手再执行”
防抖不是简单加个 setTimeout 就完事——它本质是「取消重来」的策略。用户连续输入、拖拽或缩放窗口时,你并不需要每一下都响应,而是只关心他最终停下来那一刻的状态。
常见错误现象:
• 搜索框输完立刻发请求,但用户还在打字,结果发了 5 次请求,后 4 次全白费
• resize 监听里直接查 DOM 宽高,页面卡顿甚至崩溃
• 定时器没清干净,导致旧回调在新逻辑之后执行,数据错乱
- 正确做法:每次触发前必须
clearTimeout(timeoutId),否则上一个定时器仍会执行 - 参数
delay推荐 150–300ms:太小(如 50ms)几乎不起作用;太大(如 1s)用户能明显感知延迟 - 注意
this和参数传递:用func.apply(this, args),别写成func(args)(会丢上下文和展开参数)
节流 throttle:不是“限制次数”,而是“保底节奏”
节流适用于那些你必须「过程中响应」的场景,比如滚动定位、鼠标轨迹、Canvas 动画。它不等用户停下,而是强制让回调按固定节奏跑——哪怕用户狂滚 1 秒,你也只执行 10 次(假设间隔 100ms)。
常见错误现象:
• 纯时间戳版 throttle 在用户快速滚动到底部后突然停下,最后位置没更新(漏掉结尾)
• 纯定时器版堆起多个未执行任务,松手后连发几下,体验断续
• 多个元素共用同一个 throttle 函数,计时互相干扰(比如两个滚动容器抢同一个 lastCall)
- 推荐组合实现:用时间戳判断是否该执行 + 定时器兜底保证结尾不丢失
- 每个独立监听源应有自己实例:不要
const handleScroll = throttle(fn, 100)后绑给多个元素;要为每个元素单独调用throttle - 若依赖实时坐标(如拖拽),
throttle可能视觉卡顿,此时优先考虑requestAnimationFrame
选错就白优化:debounce vs throttle 的关键决策点
这不是性能“银弹”,选错反而更糟——比如用 debounce 做滚动加载,用户已经划过“加载更多”区域才发起请求;用 throttle 做搜索联想,用户停手后还继续发前几次的请求,浪费带宽又返回过期结果。
- 用
debounce当:只关心最终值(输入完成、窗口调完、表单填完) - 用
throttle当:必须响应过程(滚动位置、拖拽坐标、绘图帧) - 需要首尾都响应?Lodash 的
throttle(func, wait, { leading: true, trailing: true })可以,但原生实现需手动补全逻辑
实际部署时最易忽略的一件事
防抖和节流函数返回的是**新函数**,必须赋值后绑定到事件上,不能直接调用。写成 window.addEventListener('scroll', throttle(handleScroll, 100)()) 是错的——多了一对括号,等于立即执行并返回 undefined。
还有:它们不解决回调函数本身低效的问题。如果 handleScroll 里反复 document.querySelector 或强制同步布局,再好的节流也救不了。先优化函数体,再套控制层。











