Sinon 假定时器 `clock.runToFrame()`:按动画帧推进时钟与测试动画逻辑
2026/9/24 14:43:55 网站建设 项目流程
  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

导读

clock.runToFrame()是 Sinon.JS 假定时器(Fake Timers)体系中面向动画与渲染节奏的专用 API:它不像tick()那样按毫秒数推进,而是把时钟精确地推进到下一个动画帧边界(标准 16ms)。读完本文,你将掌握如何用它在单元测试中模拟requestAnimationFrame驱动的动画循环、让整帧内到期的定时器一次性触发,并结合tick()等其它推进手段组合出精确的时序断言。

一、runToFrame()是什么

在 run-to-frame.md 中,官方对该 API 的定义只有一句话:

Advances the clock to the next animation frame (standard 16ms).

即:将假时钟推进到下一个动画帧,每个标准动画帧的跨度为 16 毫秒。这一语义与浏览器requestAnimationFrame的常规刷新率(约 60FPS,即每帧约 16.67ms)保持一致,文档中按标准值取 16ms。

它与同属「按帧/按目标推进」家族的runToLast()next()jump()的区别在于:

  • clock.tick(time):按指定毫秒数推进(如tick(500)),见 tick.md;
  • clock.runToLast():运行所有待处理定时器直至最后一个触发,见 run-to-last.md;
  • clock.next():只触发下一个最早的定时器;
  • clock.jump(time):跳转时间但不触发途经的定时器;
  • clock.runToFrame():按**动画帧边界(16ms 对齐)**推进时钟并触发帧内到期的定时器。

二、行为语义:对齐 16ms 帧边界

runToFrame()的核心特征是帧边界对齐:每次调用都把时钟推进到下一个 16ms 的整数倍位置。文档配套测试 run-to-frame.test.js 精确验证了这一行为:

const clock = sinon.useFakeTimers(); clock.runToFrame(); t.equal(clock.now, 16, "clock should be at 16ms after first frame"); clock.tick(3); // now at 19ms clock.runToFrame(); t.equal(clock.now, 32, "clock should be at 32ms after second frame"); clock.restore();

从测试可以提炼出两条可验证的语义:

  1. 首次调用:时钟从当前时间(默认 UNIX 纪元 0)推进到16ms
  2. 非对齐位置继续调用:先用tick(3)把时钟推到19ms,再调用runToFrame()会推进到下一个 16ms 边界,即32ms——而不是19 + 16 = 35ms。这说明它始终以帧边界为目标点,而不是简单地累加 16ms。

三、帧内到期的定时器会被触发

runToFrame()并非单纯拨动clock.now,它还会像tick()一样触发推进区间内到期的定时器。同样在 run-to-frame.test.js 中:

const clock = sinon.useFakeTimers(); let called = false; setTimeout(() => { called = true; }, 16); clock.runToFrame(); t.ok(called, "timer scheduled at 16ms should have fired"); clock.restore();

这里用全局setTimeout注册了一个 16ms 后触发的回调。由于假定时器接管了全局定时器(详见下文源码分析),该定时器被绑定到假时钟上;当runToFrame()把时钟推进到 16ms 帧边界时,这个定时器恰好到期并被触发。因此runToFrame()的完整语义是:推进到下一帧边界 + 执行该帧内所有到期的定时器

四、源码印证:requestAnimationFrame也在假定时器的接管范围内

runToFrame()之所以能服务于动画测试,前提是假定时器确实接管了浏览器/Node 环境中的动画相关 API。查看 Sinon 对假定时器的封装 src/sinon/util/fake-timers.js:

export const timers = { setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, Date: Date, }; addIfDefined(timers, "setImmediate"); addIfDefined(timers, "clearImmediate"); addIfDefined(timers, "Temporal"); addIfDefined(timers, "performance"); addIfDefined(timers, "requestAnimationFrame"); addIfDefined(timers, "cancelAnimationFrame"); addIfDefined(timers, "requestIdleCallback"); addIfDefined(timers, "cancelIdleCallback"); addIfDefined(timers, "hrtime"); addIfDefined(timers, "nextTick"); addIfDefined(timers, "queueMicrotask");

可见requestAnimationFramecancelAnimationFramerequestIdleCallbackcancelIdleCallback等渲染/空闲回调 API 都通过addIfDefined加入接管清单(仅在环境中存在时才接管)。与此同时,同一个文件揭示了runToFrame()的底层来源:

import FakeTimers from "@sinonjs/fake-timers";

即 Sinon 的假定时器是基于独立的 @sinonjs/fake-timers 包实现,clock对象(含runToFrame方法)就是由它创建的实例。useFakeTimers()在无参数时以{ now: 0 }创建时钟,因此默认从 UNIX 纪元 0 开始,首次runToFrame()恰好落在 16ms。

五、实战:结合useFakeTimers()与帧推进测试动画

要使用runToFrame(),先通过sinon.useFakeTimers()安装假时钟,测试结束后务必调用clock.restore()还原全局环境(src/sinon/util/fake-timers.jsclock.restore = clock.uninstall将二者绑定)。组合示例:

import sinon from "sinon"; const clock = sinon.useFakeTimers(); // 模拟一帧动画:每帧推进 16ms 边界 clock.runToFrame(); console.log(clock.now); // 16 // 帧中途的位置,runToFrame 仍会推进到下一帧边界 clock.tick(3); // now = 19 clock.runToFrame(); // now = 32 clock.restore();

若要精确控制起始时间,可像 use-fake-timers.md 中那样传入now或配置对象:

const clock = sinon.useFakeTimers({ now: 1483228800000, loopLimit: 10 });

loopLimit默认 1000,限制了runAll()等批量运行时的最大定时器执行数,可防止意外死循环;对按帧推进的runToFrame()而言,它只会推进一帧,不受该限制影响,但了解该配置有助于理解整套假时钟的行为边界。

六、与tick()的配合:帧粒度测试动画

文档配套的_index.test.js(见 fake-timers 总览测试)演示了动画测试的典型模式——用tick(510)验证 500ms 动画完成。在按帧驱动的场景中,runToFrame()的优势是不关心具体毫秒数,只关心帧数,例如验证「3 帧后状态变化」:

const clock = sinon.useFakeTimers(); let frameCount = 0; function frame() { frameCount += 1; requestAnimationFrame(frame); // 递归调度下一帧 } requestAnimationFrame(frame); clock.runToFrame(); clock.runToFrame(); clock.runToFrame(); console.log(frameCount); // 3 clock.restore();

这种写法对刷新率不敏感,比硬编码毫秒数的断言更贴近动画语义。

七、注意事项

  • 标准帧为 16ms:文档明确标注 "standard 16ms",如需自定义帧长,可先用tick()/jump()做非对齐推进,再让runToFrame()对齐到下一帧边界;
  • 必须成对使用 restore:安装后无论测试成功失败都应在finally中调用clock.restore(),避免污染后续用例;
  • 环境差异requestAnimationFrame仅在宿主环境存在时才被接管(addIfDefined逻辑),在纯 Node.js 无动画 API 的环境中,runToFrame()仍可正常推进帧边界并触发普通setTimeout定时器;
  • 更细粒度的替代:若只需触发下一个定时器而非按帧推进,可考虑 clock.next();若需跑完所有定时器,可考虑 clock.runToLast()。

结语

clock.runToFrame()是 Sinon 假定时器为动画/渲染类测试提供的专用推进器:以 16ms 帧边界为对齐目标,一次调用完成「推进时钟 + 触发帧内定时器」两件事。结合useFakeTimers()的配置(nowtoFakeloopLimit等)与tick()runToLast()等推进手段,可以构造出既精确又贴近真实渲染节奏的时序测试。

  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

相关推荐

上一篇:终极指南:如何用Gamdl突破Apple Music限制下载ALAC无损音乐 🎵
下一篇:Photon模板应用实战:快速启动Electron项目的捷径

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询