跳到主要内容

防抖(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 节流。