前端一面:防抖、节流与高频事件优化

“防抖/节流”已经几乎是前端一面的固定题了。这一篇用几个常见高频事件为例,讲讲这两个概念的直觉、实现方式,以及面试里常见的变体题和参考答案。

在浏览器里,有很多事件可以在极短时间内被触发多次:

  • scroll / wheel:滚动时可能每帧触发多次;
  • resize:窗口大小调整时连续触发;
  • input / keyup:快速输入时每个按键都触发;
  • mousemove:鼠标移动时频繁触发。

如果在这些事件的回调中直接做 DOM 操作、复杂计算或网络请求,很容易导致:

  • 页面卡顿、掉帧;
  • 无意义的重复请求;
  • 不必要的布局/重绘。

防抖和节流,就是两种常见的“减频”手段。

直觉:
不断开会的路人甲,只要他还在说话,就先别记笔记;等他停下来一段时间,再一次性记。

实现思路:

  • 在每次事件触发时,取消之前的定时器,重新开启一个新的;
  • 如果在设定时间内没有新的触发,就执行真正的回调。

典型应用场景:

  • 搜索框输入联想:用户停止输入 300ms 后再发请求;
  • 窗口大小调整结束后再做一次布局计算。

直觉:
红绿灯限流:在固定时间窗口内,最多只允许一批车通过。

实现思路:

  • 基于时间戳:记录上次执行时间,如果本次触发距离上次执行不足指定间隔,就忽略;
  • 基于定时器:如果当前没有定时器,就在第一次触发时开启一个,等定时器到期后再允许下一次执行。

典型应用场景:

  • 滚动时实时更新某些轻量 UI(如返回顶部按钮的显隐);
  • 鼠标移动时的提示跟随,不需要每一个像素都重新计算。
 1function debounce(fn, delay) {
 2  let timer = null;
 3  return function (...args) {
 4    const ctx = this;
 5    clearTimeout(timer);
 6    timer = setTimeout(() => {
 7      fn.apply(ctx, args);
 8    }, delay);
 9  };
10}

要点:

  • 用闭包保存 timer
  • 每次触发都先清理上一次的定时器,再重新启动;
  • 使用 apply 保留原始 this 和参数。
 1function throttle(fn, interval) {
 2  let lastTime = 0;
 3  return function (...args) {
 4    const now = Date.now();
 5    if (now - lastTime >= interval) {
 6      lastTime = now;
 7      fn.apply(this, args);
 8    }
 9  };
10}

要点:

  • 通过 lastTime 控制“多久执行一次”;
  • 在某些场景下首尾是否执行,可以通过初始化 lastTime 或补一个定时器版本来调整。

参考答案要点:

  • 防抖:在事件高频触发的情况下,只在“最后一次触发结束后的 N 毫秒”执行回调;
    • 适合搜索输入、窗口 resize 等场景;
  • 节流:保证在一段时间内回调最多执行一次,忽略其间重复触发;
    • 适合 scroll、mousemove 等场景。

可以用一句直觉性的类比加深印象:

  • 防抖 = 等人说完话再记笔记;
  • 节流 = 红绿灯控流,每隔一段时间放一批车。

可以给出前面的 debounce 简化实现,并解释关键点:

  • 使用闭包保存 timer
  • 调用时先 clearTimeoutsetTimeout
  • 延时结束再执行真正的回调。

如果想多加一分,可以提到“立即执行版防抖”(leading edge):

  • 第一次触发立即执行,后续在冷却时间内不再触发;
  • 可以通过一个布尔标志和两个分支来实现。

可以给出时间戳版本的 throttle,并补充说明:

  • 时间戳版:适合需要立即响应第一次触发的场景;
  • 定时器版:适合希望在最后一次触发后也能再执行一次的场景;
  • 实际工程里可以组合两者,做到“头尾都能执行,同时中间控制频率”。

例如:

 1function throttleBoth(fn, interval) {
 2  let lastTime = 0;
 3  let timer = null;
 4
 5  return function (...args) {
 6    const now = Date.now();
 7    const remaining = interval - (now - lastTime);
 8    const ctx = this;
 9
10    if (remaining <= 0) {
11      if (timer) {
12        clearTimeout(timer);
13        timer = null;
14      }
15      lastTime = now;
16      fn.apply(ctx, args);
17    } else if (!timer) {
18      timer = setTimeout(() => {
19        lastTime = Date.now();
20        timer = null;
21        fn.apply(ctx, args);
22      }, remaining);
23    }
24  };
25}

在一面环境下,不一定要写到这么完整,但如果你能把“什么时候用防抖/节流、核心实现思路、常见变体”讲顺,已经算是这一块的高分回答了。