真实面经题目 · 原创解析

节流函数如何实现?

这道题考察的不是背出一个节流函数片段,而是能否清楚说明高频事件限流的边界语义:节流与防抖的区别、时间戳方案和定时器方案的触发时机差异、是否支持首次立即执行与最后一次补偿执行,以及如何正确保留 this、参数、取消能力和返回值限制。

出现于:阿里巴巴 · 前端

60 秒回答模板

节流的核心是让一个高频触发的函数在固定时间窗口内最多执行一次,常见于 scroll、resize、mousemove、按钮连续点击等场景。它和防抖不同,防抖强调事件停止一段时间后才执行,节流强调持续触发期间按固定频率执行。实现上我会讲两种思路:第一种是时间戳比较,每次触发时用当前时间减去上次真正执行时间,超过间隔就立即执行并更新上次时间,这种天然支持 leading,但通常没有 trailing;第二种是定时器方案,触发时如果没有定时器就设置一个延迟任务,任务到点后执行并清空定时器,这种更偏 trailing,也能稳定把最后一次参数带上。完善版还要考虑保留调用时的 this 和 arguments,支持取消定时器、清理状态,并说明返回值在异步 trailing 场景下不能像普通函数一样同步拿到。

考点 题目本质
主线 节流与防抖
易错点 把节流解释成停止触发后才执行,混淆了防抖和节流的核心语…

深入解析

01

题目本质

节流函数的本质是给高频调用加一个执行频率上限,而不是简单地延迟执行。面试中要先说明业务动机:浏览器事件可能在短时间内触发几十次甚至上百次,如果每次都做布局计算、接口请求或状态更新,会造成性能抖动。节流通过固定时间窗口只允许一次真实执行,把不可控的事件频率转成可控的处理频率。

02

节流与防抖

节流和防抖经常一起考,但语义完全不同。防抖是在连续触发后只响应最后一次,典型例子是搜索框输入结束后再请求;节流是在连续触发过程中按固定间隔持续响应,典型例子是滚动过程中每隔一段时间计算位置。回答时如果只说“减少执行次数”是不够的,要强调防抖看的是停止时间,节流看的是执行频率。

03

时间戳方案

时间戳节流的思路是记录上一次真正执行的时间,每次外层函数被触发时读取当前时间,判断两者差值是否达到等待间隔。如果达到,就立即调用原函数并更新上次执行时间;如果没有达到,就直接忽略本次触发。它的优点是简单、首次触发通常可以立即执行,适合按钮防连点或希望即时响应的场景;缺点是持续触发结束时,最后一次触发不一定会被补执行。

04

定时器方案

定时器节流的思路是用一个 timer 标记当前窗口内是否已经安排了执行任务。事件触发时如果没有 timer,就创建一个延时任务;在延时结束时执行原函数,然后清空 timer,让下一轮触发可以重新安排。它的优势是节奏稳定,并且可以更自然地保留最后一次触发的参数;但默认不会在第一次触发时立即执行,用户感知上可能比时间戳方案慢半拍。

05

leading 与 trailing

成熟实现需要明确 leading 和 trailing 两个选项。leading 表示时间窗口开始时是否立即执行,trailing 表示窗口结束时是否用最后一次调用补执行。时间戳方案偏 leading,定时器方案偏 trailing;工业级工具函数通常会组合两者,既能首次立即响应,又能保证最后一次状态不丢。面试时主动说出这两个语义,会比只写一个简化版本更完整。

06

上下文与参数

节流包装函数不能丢失原函数的调用上下文和参数。因为在真实业务里,原函数可能依赖 this 指向组件实例、DOM 对象或某个服务对象,也可能依赖每次事件传入的 event、坐标、输入值等参数。实现时应在包装函数被调用时保存当前 this 和参数,并在真正执行原函数时用正确方式调用,而不是直接调用导致 this 指向变化。

07

取消与返回值

可取消能力是高级追问常见点,尤其在组件卸载、路由切换或弹窗关闭时,需要清掉尚未执行的定时器并重置内部状态,避免无意义执行或访问已销毁对象。返回值也要说明边界:如果是立即执行分支,可以同步拿到原函数返回值;如果是定时器延后执行,外层调用当下无法同步返回最终结果,通常不能依赖节流函数的返回值做关键逻辑。

08

渲染类变体

对于 scroll、resize、mousemove 这类和页面渲染强相关的场景,还可以提 requestAnimationFrame 版本。它不是按固定毫秒数限流,而是把执行合并到浏览器下一帧绘制前,通常更适合读取布局、更新样式或做轻量动画。它的优点是与屏幕刷新节奏对齐,减少不必要的中间帧计算;但它不适合要求严格时间间隔的接口调用限流。

javascript

带首尾触发的节流函数

function throttle(fn, wait) {
  let lastRun = 0;
  let timer = null;

  return function throttled(...args) {
    const now = Date.now();
    const remaining = wait - (now - lastRun);

    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastRun = now;
      fn.apply(this, args);
      return;
    }

    if (!timer) {
      timer = setTimeout(() => {
        lastRun = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}
  • 立即执行保证第一次交互有反馈,尾部定时器保证最后一次参数不丢。
  • 真实项目里还可以补 cancel/flush,但面试最小实现先讲清时间窗和 this/参数透传。

易错点

  • 把节流解释成停止触发后才执行,混淆了防抖和节流的核心语义。
  • 只写定时器但不清空 timer,导致后续触发被永久拦截或执行状态混乱。
  • 没有保存 this 和参数,包装类方法或事件回调时会出现上下文丢失问题。
  • 默认认为节流函数一定能同步返回原函数结果,忽略 trailing 异步执行的限制。
  • 没有说明 leading 和 trailing,导致实现语义不清,边界行为无法被调用方预期。
  • 组件销毁时不提供取消能力,残留定时器可能触发无效更新或多余副作用。

面试官追问

节流和防抖最核心的区别是什么?

节流关注执行频率,连续触发时每隔固定时间最多执行一次,所以事件一直发生也会周期性响应;防抖关注停止触发,只有在最后一次触发后等待一段时间才执行,更适合输入完成、窗口调整结束这类场景。

为什么时间戳实现通常没有 trailing?

时间戳实现每次只判断当前时间和上次执行时间的差值,未达到间隔就直接忽略本次调用。如果用户在间隔结束前停止触发,就不会再有新的调用来触发判断,因此最后一次参数不会自动补执行。

如何同时支持首次立即执行和最后一次补执行?

可以把时间戳判断和定时器补偿结合起来:窗口开始时满足 leading 就立即执行;窗口内的新调用记录最新 this 和参数;如果开启 trailing,则在剩余时间结束后用最新参数再执行一次。

为什么要提供 cancel 方法?

因为定时器节流可能存在尚未执行的任务,组件卸载、页面跳转或业务取消时如果不清理,延迟任务可能访问失效状态,造成内存泄漏、异常更新或多余请求。cancel 用来清定时器并重置内部记录。

requestAnimationFrame 节流适合什么场景?

它适合滚动、拖拽、窗口尺寸变化等渲染相关处理,因为执行会对齐浏览器刷新帧,避免一帧内多次计算和绘制。但它不保证固定毫秒间隔,不适合作为接口请求或业务规则的精确定时限流。