DEBUG.WIKI
腾讯云;让你的想法跑起来;轻量服务器 · 建站 / Docker / API;查看产品 →
晚安云;香港 CN2 ¥15/月
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →

fake-timers 的 idle 回调没执行,runToLast 又报负时间

fake-timers 15.4.0 将 idle 回调排在较晚的普通定时器之后,可能导致 tick 漏回调或 runToLast 报负时间。区分调度替身、排序补丁与带 timeout 的取消问题。

测试里安排了一个 idle 回调和几个普通定时器,clock.tick() 只执行后者;再调用 runToLast(),可能直接停在错误时刻,或报 Negative ticks are not supported。如果使用 @sinonjs/fake-timers 15.4.0,并且 idle 回调早于仍待执行的普通定时器,应检查内部排序。

15.4.0 的比较器先把 IdleCallback 排在其他类型之后,再比较 callAt。于是一个本应较早触发的 idle 项,可以排到很晚的 timeout 后面。tick 推进下界以后,早先的 idle 项已经落在范围之外,并没有按预期运行。

一直 runAll 不能纠正顺序

runToLast 需要从定时器结构取得最后一项。排序已经不是单纯的时间顺序,继续扩大 tick 区间或重复调用 runToLast,不能保证得到正确末尾,也不应据一次执行结束就认定回调顺序正确。

先记录安装的 fake-timers 副本、安排回调的位置和预计 callAt。idle 在某个 timeout 内部才创建时,其创建时刻与独立安排在零时刻不同;不要删掉这些先后条件以后,用另一个成功样本否定故障。

浏览器的 requestIdleCallback 本来也不是精确到点执行的普通 timeout。测试要分清业务关心的是“最终做了空闲任务”,还是浏览器真实的空闲预算、超时与竞争顺序。

业务单测可注入受控的空闲调度边界

若只想测试空闲任务中的业务逻辑,可给现有模块注入一个调度器:测试保存待执行回调,在确认其他业务步骤后显式触发;取消时从待执行集合移除。这样无需依赖这条有问题的 idle 排序。

这类替身只能证明应用如何登记、取消和处理任务,不能证明浏览器何时有空闲时间。不要简单将所有 requestIdleCallback 替换成 setTimeout(0),却仍声称覆盖了 idle 语义。真实调度与 deadline 相关的集成测试应保留在相应浏览器环境。

确实需要 fake-timers 同时驱动两类任务时,可在锁定版本的依赖补丁中检查比较器:先比较 callAt,仅在同一触发时刻再应用 idle 优先级。应将补丁纳入项目原有的包补丁与代码审查流程,保留同一时刻的顺序规则;这不是 15.4.0 已包含的正式修复,也不能随手调整运行中 clock 的内部堆。

保留变更前的锁文件与包来源;撤回时移除该局部补丁,按原锁文件恢复依赖,再重验原来的调度与清理行为。

带 timeout 的取消异常另看类型

如果报的是 Cannot clear timer: timer created with setTimeout() but cleared with cancelIdleCallback(),还涉及另一个分支:带非零 timeout 的 idle 请求被标成 Timeout,取消却要求 IdleCallback。仅改变排序不能解决类型不匹配,也不要混用 clearTimeout 来掩盖应用的取消接口。

截至 2026 年 10 月 26 日,正式 15.4.0 仍保留上述排序与类型处理。验收顺序修订时,覆盖较早 idle 加较晚 timeout、同一时刻两类任务、正常取消和 runToLast 的最终时刻;若需要带 timeout 的 idle 请求,应单独验证其取消行为,不能只检查普通定时器都运行过。