Frontend SWE / Supply Chain · Phone JavaScript

Apple 前端手写 Debounce:最后一次参数、this 与取消

以 trailing-only 训练契约实现单定时器 debounce,并用可控虚拟时钟验证调用顺序。

题目重点:以 trailing-only 训练契约实现单定时器 debounce,并用可控虚拟时钟验证调用顺序。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版为 trailing-only:一段连续调用静默 delayMs 后才调用一次,使用最后一次的参数和 this;delayMs≥0。
  • 提供 `cancel()` 作为清理接口但不实现 flush、leading、maxWait 或异步结果转发。原面试若要求这些功能,需要重新签行为。

解题路线

  1. 闭包只保留一个 timer、最后一次参数和 this。新调用到达时清旧 timer 并重置到新到达时刻+delayMs。
  2. timer 触发时先把 timer 标为无待执行,取出最后参数/this,再用 `callback.apply` 执行;这样 callback 内部再次触发包装函数也可重新安排。
  3. cancel 清理 timer 与所捕获引用,避免组件卸载后仍执行或持有大型对象。
  4. 用自制虚拟时钟验证 t=0、t=2 调用在 t=7 只执行最后一次,另测 this、实参、cancel 与重新启动;不靠真实 sleep 导致不稳定测试。

手动走一遍

等待 5ms:t=0 输入 A,t=2 输入 B,则 A 的 t=5 定时器被取消,B 在 t=7 执行一次。若 t=7 前又输入 C,执行时间再次顺延。throttle 则通常限制频率,可能在持续输入期间定期执行,与本训练版不同。

JavaScript 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

export function debounce(callback, delayMs, timers = {
  setTimeout: globalThis.setTimeout,
  clearTimeout: globalThis.clearTimeout,
}) {
  if (typeof callback !== "function" || delayMs < 0) {
    throw new TypeError("callback must be a function and delay non-negative");
  }
  let timer = null;
  let lastArgs = null;
  let lastThis = null;

  function debounced(...args) {
    lastArgs = args;
    lastThis = this;
    if (timer !== null) timers.clearTimeout(timer);
    timer = timers.setTimeout(() => {
      timer = null;
      const argsToCall = lastArgs;
      const context = lastThis;
      lastArgs = lastThis = null;
      callback.apply(context, argsToCall);
    }, delayMs);
  }

  debounced.cancel = () => {
    if (timer !== null) timers.clearTimeout(timer);
    timer = null;
    lastArgs = lastThis = null;
  };
  return debounced;
}

复杂度

每次调用 O(1) 调度与取消、只保留一个定时器;额外内存为最近参数引用的大小。真实定时器精度受事件循环与页面后台节流影响,不能保证精确毫秒时刻。

易错点与追问

容易失分的边界

  • 每次新建 timer 但不取消旧 timer,变成延迟调用多次。
  • 用箭头函数包装而无意丢失调用时的 this。
  • 保留第一次参数而非最后一次参数。
  • 组件卸载不 cancel,回调对已释放 UI 仍更新状态。
  • 题目只说 debounce,却把 leading/trailing 的某一种实现冒充唯一要求。

继续深挖

  • leading+trailing 同开时,单次调用应执行几次?
  • 如何加 maxWait 避免持续输入永远不执行?
  • 若回调返回 Promise,包装函数怎样暴露每次调用结果?
  • React 中怎样避免每次 render 创建新 debounce 实例使历史 timer 失效?
✓已复制微信号!