题目重点:以 trailing-only 训练契约实现单定时器 debounce,并用可控虚拟时钟验证调用顺序。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练版为 trailing-only:一段连续调用静默 delayMs 后才调用一次,使用最后一次的参数和 this;delayMs≥0。
- 提供 `cancel()` 作为清理接口但不实现 flush、leading、maxWait 或异步结果转发。原面试若要求这些功能,需要重新签行为。
解题路线
- 闭包只保留一个 timer、最后一次参数和 this。新调用到达时清旧 timer 并重置到新到达时刻+delayMs。
- timer 触发时先把 timer 标为无待执行,取出最后参数/this,再用 `callback.apply` 执行;这样 callback 内部再次触发包装函数也可重新安排。
- cancel 清理 timer 与所捕获引用,避免组件卸载后仍执行或持有大型对象。
- 用自制虚拟时钟验证 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 失效?
