题目重点:保留最后参数和this,按上次实际执行计冷却,处理定时器延迟、边界、取消与重入。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 第一调用立即执行,wait内只缓存最后一次,窗口到期执行该缓存;若没有再次调用,不补一次重复执行。
- 窗口从实际callback执行时起算,wait=0每次立即;同刻测试按已到期timer先运行。
- 同步callback,压制调用返回undefined,立即调用返回callback结果;cancel丢待执行并重置窗口。
解题路线
- 区分debounce:后者常在最后一次输入后等完整间隔;本题窗口锚定上次执行,500/700ms的输入不能把第一轮尾触发拖到1700ms。
- 状态是last执行时间、至多一个timer、pending的args与receiver。首次或已过wait直接执行;未过窗口只覆盖pending,第一次受抑调用才设置剩余时间timer。
- timer触发先清timer和pending,再更新last并调用缓存。清理必须在callback之前,避免callback重入时新建的pending/timer被随后旧清理覆盖。
- 同步执行路径若发现旧timer仍待运行,要取消并丢旧pending,再使用当前新调用参数;否则延迟事件循环可能导致旧尾调用紧跟新执行,违反间隔合同。
- 尾调用更新last为实际执行时刻,形成新的冷却;定时器只能保证不早于期限,主线程繁忙可能晚到,不承诺精准1000ms真实时间。测试使用注入虚拟时钟控制逻辑顺序。
- this必须随最后一次调用保存,不能只保存args;对象方法包装会依赖receiver。cancel清理定时器并重置last,未加cancel/flush的功能不要默认存在。
- 异常与异步需另定义:本实现同步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与调用同刻如何保证一致政策?
