先记住这个答案
防抖把一段连续调用合并,常见的尾沿防抖会在最后一次调用后等待指定时间再执行;再次触发就重新计时。节流限制执行频率,连续触发时仍会按策略间隔执行。搜索联想常用防抖,滚动位置采样常用节流;是否立即执行、是否补最后一次,都需要单独约定。
- 防抖:停止触发后再执行
- 节流:持续触发时限制频率
- 先说清首沿、尾沿和取消规则
防抖和节流的区别,先用时间线说明
假设调用发生在 0、80、160、400ms,等待间隔是 200ms。尾沿防抖的前三次调用被合并,计划在 360ms 执行;400ms 的新调用再计划于 600ms 执行。这里写的是理想调度时间,主线程繁忙时实际执行可能更晚。
若采用只执行首沿的节流,0ms 立即执行,80ms 和 160ms 被忽略,400ms 再执行。若增加尾沿补发,结果会不同。因此面试时先声明 leading 和 trailing 策略,否则两份看似不同的输出可能都符合各自的实现。
- 搜索联想 / 输入校验
- 通常更关心停下来后的最终输入,用尾沿防抖减少中间请求。持续输入不能无限延后时,可设计 maxWait 上限。
- 滚动位置 / 拖动采样
- 需要操作过程中持续反馈,用节流限制执行频率;与屏幕动画同步的更新还应考虑 requestAnimationFrame。
手写防抖:保留 this、参数与 cancel
下面实现的是尾沿 debounce:每次调用取消旧定时器并重新安排。返回普通函数保留调用方的 this,定时器回调用箭头函数捕获这次调用的 this 和 args。cancel 用于页面离开或交互撤销时清理待执行任务。
验证“防抖与节流”中的“手写防抖:保留 this、参数与 cancel”时,应固定输入、运行环境与触发顺序,并同时记录正常结果和失败结果。这样能确认本节结论来自目标机制,而不是缓存、旧状态或偶然时序。
function debounce(fn, wait) {
let timer;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
timer = undefined;
fn.apply(this, args);
}, wait);
}
debounced.cancel = () => {
clearTimeout(timer);
timer = undefined;
};
return debounced;
}
const search = debounce(query => console.log(query), 200);
search('n');
search('ne');
search('next');查看输出与解释
next同步连续调用只留下最后一个定时器,至少等待指定时间后打印 next。调用 search.cancel() 可取消尚未执行的这次任务;此版本不提供延迟函数的同步返回值。
手写节流:先明确首沿执行策略
下面的 throttle 用 performance.now() 测量时间间隔,首次立即执行,间隔内忽略新调用,不补尾沿。它适合讲清核心思路;如果停止操作后必须更新最后一个位置,需要扩展尾沿逻辑,不能把这个版本直接当作所有场景的完整实现。
验证“防抖与节流”中的“手写节流:先明确首沿执行策略”时,应固定输入、运行环境与触发顺序,并同时记录正常结果和失败结果。这样能确认本节结论来自目标机制,而不是缓存、旧状态或偶然时序。
function throttle(fn, wait) {
let lastTime = -Infinity;
return function throttled(...args) {
const now = performance.now();
if (now - lastTime >= wait) {
lastTime = now;
return fn.apply(this, args);
}
};
}
const report = throttle(value => console.log(value), 200);
report('first');
report('second');查看输出与解释
first正常连续执行且间隔小于 200ms 时,只输出 first。这里没有尾沿定时器,停止调用也不会自动补发 second;若调试断点使两次调用相隔足够久,则两次都会执行。
实际使用还要处理生命周期与请求竞态
不要在每次 input 事件里重新调用 debounce 创建包装函数,那样每次都有独立 timer,旧任务无法被清除。在 React 中也要让同一段交互复用包装函数,同时处理回调捕获旧状态的问题;组件卸载或依赖切换时取消仍在等待的调用。
防抖只减少发起请求的次数,不能保证响应按发出顺序返回。如果旧查询较慢、新查询较快,旧响应仍可能覆盖新结果。可以用递增请求序号判断返回结果是否仍有效,或者通过 AbortController 取消已经过时的请求;cancel 定时器不会自动取消已发出的网络请求。
容易答错的地方
- “防抖总是执行最后一次”
- 这是常见尾沿策略的表现;首沿防抖会先执行。说明 leading、trailing 和 maxWait,比只背“最后一次”更准确。
- “节流每隔 wait 必定执行”
- 它约束的是调用频率,不是独立运行的时钟。没有新触发且没有尾沿任务时,不会凭空执行;真实计时器也可能延迟。
- “减少请求就解决了竞态”
- 发起频率和返回顺序是两个问题。输入变化后,已经发出的旧请求还需要取消或在写入结果时进行有效性检查。
面试官还会怎么问?
为什么防抖返回的函数不能直接写成箭头函数?
如果需求是透传调用方的 this,返回普通函数可以接收调用时的 this;返回箭头函数则会捕获创建它的外层 this,调用方通过对象方法或 call 传入的新 this 不会生效。若原回调完全不依赖 this,也可以使用明确不支持 this 透传的简化版本。
cancel 和 flush 有什么区别?
cancel 丢弃尚未执行的尾沿任务;flush 则立即执行已经等待中的调用,通常还要返回结果。本页防抖仅实现 cancel。如果要加 flush,需要额外保存最后的参数和上下文,处理没有待执行任务、重复调用和清理状态的边界。
一直输入时,怎样保证最终会触发搜索?
单纯尾沿防抖可能被持续输入无限推迟。如果交互要求持续输入期间也周期性得到结果,可引入 maxWait 限制最长等待时间,或选择合适的节流策略。需求应同时定义首次反馈、最后一次输入和最长等待三个条件。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。