SDET / Frontend · Coding

TikTok Throttle:立即执行与窗口末最后一次调用

保留最后参数和this,按上次实际执行计冷却,处理定时器延迟、边界、取消与重入。

题目重点:保留最后参数和this,按上次实际执行计冷却,处理定时器延迟、边界、取消与重入。

题意与假设

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

  • 第一调用立即执行,wait内只缓存最后一次,窗口到期执行该缓存;若没有再次调用,不补一次重复执行。
  • 窗口从实际callback执行时起算,wait=0每次立即;同刻测试按已到期timer先运行。
  • 同步callback,压制调用返回undefined,立即调用返回callback结果;cancel丢待执行并重置窗口。

解题路线

  1. 区分debounce:后者常在最后一次输入后等完整间隔;本题窗口锚定上次执行,500/700ms的输入不能把第一轮尾触发拖到1700ms。
  2. 状态是last执行时间、至多一个timer、pending的args与receiver。首次或已过wait直接执行;未过窗口只覆盖pending,第一次受抑调用才设置剩余时间timer。
  3. timer触发先清timer和pending,再更新last并调用缓存。清理必须在callback之前,避免callback重入时新建的pending/timer被随后旧清理覆盖。
  4. 同步执行路径若发现旧timer仍待运行,要取消并丢旧pending,再使用当前新调用参数;否则延迟事件循环可能导致旧尾调用紧跟新执行,违反间隔合同。
  5. 尾调用更新last为实际执行时刻,形成新的冷却;定时器只能保证不早于期限,主线程繁忙可能晚到,不承诺精准1000ms真实时间。测试使用注入虚拟时钟控制逻辑顺序。
  6. this必须随最后一次调用保存,不能只保存args;对象方法包装会依赖receiver。cancel清理定时器并重置last,未加cancel/flush的功能不要默认存在。
  7. 异常与异步需另定义:本实现同步callback异常传播但状态已更新,Promise结果不供受抑调用共享;若需要所有调用返回同一Promise或限制in-flight,必须扩展合同。

手动走一遍

0ms调用(1,2)立即得3;500ms(2,3)被压制;700ms(3,4)覆盖待执行;1000ms执行(3,4)得7。只有0ms一次则不在1000ms重跑。尾触发后1100ms新调用仍在下一窗口内。

JavaScript 参考实现

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

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

export function throttle(callback, wait, clock = {
  now: () => Date.now(),
  set: (fn, ms) => setTimeout(fn, ms),
  clear: id => clearTimeout(id),
}) {
  if (!Number.isFinite(wait) || wait < 0) throw new RangeError('wait must be nonnegative');
  let last = null, timer = null, pending = null;
  function invoke(args, receiver) {
    last = clock.now();
    return callback.apply(receiver, args);
  }
  function onTimer() {
    timer = null;
    const job = pending;
    pending = null;
    if (job) invoke(job.args, job.receiver);
  }
  function wrapped(...args) {
    const now = clock.now();
    if (last === null || now - last >= wait) {
      if (timer !== null) clock.clear(timer);
      timer = null;
      pending = null;
      return invoke(args, this);
    }
    pending = {args, receiver: this};
    if (timer === null) timer = clock.set(onTimer, wait - (now - last));
    return undefined;
  }
  wrapped.cancel = () => {
    if (timer !== null) clock.clear(timer);
    timer = null;
    pending = null;
    last = null;
  };
  return wrapped;
}

复杂度

每次输入常量状态更新O(1),内存O(最后参数引用大小)及一个timer,不累计历史调用。事件循环延迟与callback工作另计;保存参数对象不深复制。

易错点与追问

容易失分的边界

  • 每次重设wait造成debounce。
  • 保留第一份而非最后参数。
  • 尾callback后才清pending破坏重入。
  • 只传args丢this。

继续深挖

  • 需要flush如何定义?
  • callback返回Promise如何传播?
  • timer与调用同刻如何保证一致政策?
✓已复制微信号!