
react-spring 的帧循环引擎 react-spring/rafz跨应用协调 requestAnimationFrame 的轻量实现解析【免费下载链接】react-spring✌️ A spring physics based React animation library项目地址: https://gitcode.com/gh_mirrors/re/react-spring导读react-spring/rafz是 react-spring 仓库中的核心基础设施包位于 packages/rafz它把整个应用的requestAnimationFrame调用收敛到单一、可协调的帧循环中支持任务批量调度、超时timeout、错误隔离、跨库协调以及按需demand帧驱动模式。本文以 packages/rafz/README.md 为骨架结合 packages/rafz/src/index.ts 等源码与测试完整讲解它的 API、队列模型、帧循环调度顺序、raf.throttle节流器以及它在 react-spring 核心动画管线SpringValue、Controller、useScroll 等中的真实调用方式。读完本文你将掌握如何独立使用 rafz 编写高性能动画循环、如何与 React 的批量更新机制对接以及如何在始终运行与按需驱动两种帧循环模式下做出正确选择。rafz 是什么一个 700B 的协调式帧循环浏览器只提供requestAnimationFrame这样一个低级原语每次调用只注册一个回调多个动画库各自调用时会相互抢占帧造成重复测量布局、抖动或丢帧。rafz 的做法是提供一个全局单例调度器把应用内所有需要逐帧执行的任务汇总到同一条帧循环里统一驱动。注rafz 是 react-spring 对同名单体库作者 Josh Ellis的 fork见 packages/rafz/package.json 中 react-springs fork of rafz one frameloop to rule them all 的描述。README 中列出它的核心特性体积极小小于 700 bytesmingzip超时支持可在最近的帧上执行定时回调批量调度支持可对接如ReactDOM.unstable_batchedUpdates未捕获错误被隔离单个任务抛错不会中断整条帧循环持续运行减少帧跳过frame skips。这些特性在源码里都有直接对应体积小源于 packages/rafz/src/index.ts 仅用几组Set队列 一个loop函数实现整个调度器错误隔离来自eachSafely见下文持续运行则体现在raf(() true)这种返回true的常驻任务会让循环保持活动。完整 API 一览以下是 README 中给出的 API 速查括号内标注了对应的类型签名完整类型定义见 packages/rafz/src/types.tsimport { raf } from react-spring/rafz // 调度一个更新每帧传入 dt 增量时间返回 true 则下一帧继续 raf(dt {}) // FrameUpdateFn: (dt: number) boolean | void // 启动一个更新循环 raf(dt true) // 取消一个已调度的更新 raf.cancel(fn) // 调度一次写任务如 DOM 变更每帧仅执行一次 raf.write(() {}) // FrameFn: () boolean | void // 在任何更新执行之前 raf.onStart(() {}) // 在任何写操作执行之前 raf.onFrame(() {}) // 在所有写操作执行之后 raf.onFinish(() {}) // 设置一个在最近帧上执行的超时 raf.setTimeout(() {}, 1000) // 返回 { time, handler, cancel } // 替换底层 requestAnimationFrame 实现如注入 polyfill raf.use(require(essentials/raf).raf) // 获取当前时间默认 performance.now否则 Date.now raf.now() // number // 设置帧循环模式demand按需| always始终 raf.frameLoop demand | always此外README 的 Notes 节与 types.ts 还揭示了两个额外成员它们在 react-spring 内部扮演重要角色// 在回调内部禁用调度内部任务立即执行仅用于你知道自己在做什么的场合 raf.sync(fn {}) // 覆盖批量更新行为默认 fn fn()React 环境可换成 ReactDOM.unstable_batchedUpdates raf.batchedUpdates fn fn() // 错误处理钩子默认 console.error任务抛错时被调用 raf.catch console.error以及两个供高级宿主host使用的成员见下文demand 模式小节raf.advance() // 仅当 frameLoop demand 时手动推进一帧 raf.onDemand() // demand 模式下有帧工作待处理时被调用默认空操作帧循环的内部运行机制五条队列 一个循环从 packages/rafz/src/index.ts 可以看到调度器内部维护了 5 条任务队列分别对应 5 个调度入口队列入口用途对应阶段updateQueueraf(fn)更新 JS 状态推进动画、计算插值update 阶段writeQueueraf.write(fn)更新原生状态写 DOM、改样式write 阶段onStartQueueraf.onStart(fn)每帧最先执行的前置钩子帧首onFrameQueueraf.onFrame(fn)在写操作之前执行的钩子write 前onFinishQueueraf.onFinish(fn)在写操作之后执行的钩子write 后外加一个独立的timeouts数组用于超时调度。makeQueue用双Setnext/current实现队列切换并在add/delete/flush时维护pendingCount从而精确跟踪是否还有待办任务。一帧update函数的执行顺序如下flush 超时把时间已到的 timeout 一次性取出并按注册顺序安全执行eachSafely若没有剩余待办任务则stop()停止循环并返回onStartQueue.flush()执行帧首钩子updateQueue.flush(prevTs ? Math.min(64, ts - prevTs) : 16.667)执行更新任务传入 dt 增量时间首帧默认 16.667ms后续帧封顶 64ms 以防 tab 切回时 dt 过大onFrameQueue.flush()writeQueue.flush()执行写操作onFinishQueue.flush()若 demand 模式下仍有待办任务调用raf.onDemand()通知宿主请求下一帧。循环的启停由ts标记控制start()仅在ts 0时注册原生nativeRaf(loop)loop每帧先注册下一帧再通过raf.batchedUpdates(update)执行一帧内容。只要队列里有任务帧循环就会持续运转这正是 README 所说减少帧跳过的含义。两个关键设计错误隔离eachSafelyflush遍历当前 Set 时对每个任务用try/catch包裹出错时调用raf.catch(e)默认console.error。某个任务抛错不会中断后续任务也不会让整条循环崩溃。单次调度去重README 明确一个函数在一帧里只能被调度一次重复调度是 no-op。这来自Set.add的天然去重同时pendingCount只在元素真正新增时递增避免重复调度导致循环无法停止。任务的持续运行与取消返回true继续运行任何 handler超时除外返回true时flush会把它重新加入下一帧的队列fn(arg) next.add(fn)。这就是启动一个更新循环的机制——raf(() true)会让任务常驻。raf.cancel(fn)同时从 5 条队列中删除该函数。README 特别提醒它只对raf系列 handler 生效对raf.setTimeout返回的 timeout 无效timeout 需要用返回对象上的cancel()。raf.sync(fn)把sync置为 true 后回调内任何raf(...)/raf.write(...)调度都会跳过排队立即以fn(0)执行之后再恢复。这是一个逃生舱适合确定性的同步场景如某些测试环境。超时setTimeout与 react-spring 的 delay 实现raf.setTimeout(handler, ms)内部用raf.now() ms计算目标时间通过findTimeout按时间升序插入timeouts数组并立即start()启动循环返回一个{ time, handler, cancel }对象。每一帧首先 flush 到期的超时因此超时回调永远在当帧的任何 update 之前执行——这是 README 强调的Timeout handlers run first on each frame。这个机制直接支撑了 react-spring 动画的delay属性。在 packages/core/src/scheduleProps.ts 中// 暂停时缓存剩余延迟 delay timeout.time - raf.now() // 恢复时重新调度 if (delay 0 !G.skipAnimation) { state.delayed true timeout raf.setTimeout(onStart, delay) ... }可见delay并非setTimeout那种精确到毫秒的定时器而是在最近的帧上、先于所有 update 执行的帧对齐延迟暂停pause时用timeout.time - raf.now()精确缓存剩余时间恢复时重新入队。raf.throttle每帧限流一次raf.throttle(fn)包装一个函数使其每帧最多执行一次同一帧内多次调用时只有最后一次调用的参数生效。其实现packages/rafz/src/index.ts核心是把最近一次参数存入lastArgs并raf.onStart(queuedFn)——由于 onStart 队列的 Set 去重一帧内重复调用只会入队一次。let log raf.throttle(console.log) log(1) log(2) // 尚未输出 raf.onStart(() { // 此时 2 才被输出 }) // 取消待执行的调用 log.cancel() // 访问被包装的原始函数 log.handler需要注意被包装函数的this绑定不会保留返回对象上挂载了handler原始函数和cancel取消待执行调用。类型定义见 packages/rafz/src/types.ts 中的ThrottledTT { handler: T; cancel: () void }。如何与 React 批量更新对接React 的事件回调会做自动批量更新但 rafz 的帧循环在 React 的调度体系之外运行逐帧触发多个状态更新可能造成多次渲染。README 给出的方案是覆盖raf.batchedUpdates// 让一帧内的所有状态更新合并为一次 React 渲染 raf.batchedUpdates ReactDOM.unstable_batchedUpdatesreact-spring 核心包正是这样做的在 packages/core/src/SpringValue.ts、packages/core/src/Interpolation.ts 中凡是会触发用户侧状态回流的操作都包裹在raf.batchedUpdates(() ...)中如 SpringValue.ts 的 402、430、503、850、950 行Interpolation.ts 的 96 行runAsync.ts 的 189 行Controller.ts 的 499 行避免一帧内多次 setState 导致过度渲染。此外React 18 通过ReactDOM.createRoot自动批处理因此默认的fn fn()在新版本 React 下通常已足够unstable_batchedUpdates主要面向 React 18 之前的版本或非根渲染场景。若你的环境从未定义window.requestAnimationFrame如某些 SSR/测试环境需要用raf.use(polyfill)注入实现。frameLoop 模式always 与 demandraf.frameLoop控制帧循环的驱动方式默认always见 packages/rafz/src/index.tsalways默认只要有待办任务start()就调用nativeRaf(loop)由浏览器事件循环持续驱动。react-spring 的 DOM 动画react-spring/web默认使用此模式。demand帧循环不再自行请求帧而是通过raf.advance()手动推进每次调度新任务或每帧结束后若仍有待办工作会调用raf.onDemand()通知宿主host请求下一帧。这适用于由宿主驱动渲染的场景例如react-spring/three在 react-three-fiberr3f的frameloopdemand下运行——宿主只在需要时渲染rafz 通过onDemand把还有动画在跑的信号转成一次invalidate()帧请求从而避免动画在宿主不渲染的帧之间停摆。raf.advance()在非 demand 模式下调用会输出警告packages/rafz/src/index.ts并直接执行一帧update()。onDemand的触发点有两处schedule中调度新任务时以及update末尾发现pendingCount 0时源码注释明确flush 通过队列内部的add重新入队、绕过了schedule所以这是持续动画唯一的信号来源。源码级佐证demand 模式下的循环驱动packages/core/src/FrameLoopDemand.test.ts 是一个针对 issue #2402 的回归测试在裸 demand 宿主没有任何 useFrame 订阅者上loop: true的 to-array 动画必须无需用户任何 workaround 就能持续循环。测试中的DemandHost模型化 r3f v9 的循环宿主通过onDemand拿到有待办工作信号后用Promise.resolve().then(this.invalidate)把帧请求推迟到当前帧之后——因为 r3f 的addEffect运行在其 before 阶段若立即invalidate()会在同一 tick 内被update()的递减抵消导致下一帧被取消延迟后则能干净地重启循环。这印证了 types.ts 中对onDemand的注释demand 模式的宿主如react-spring/three把它接到请求下一帧上动画才不会在宿主渲染的帧之间停滞。rafz 在 react-spring 内的真实调用链rafz 是 react-spring 所有动画包共用的帧驱动器调用关系可以概括为核心包packages/core/src/globals.ts 引入Globals与frameLoop.advance后者在 packages/shared/src/FrameLoop.ts 中按优先级从低到高推进所有OpaqueAnimationstart/sort/advance并返回currentFrame.length 0作为是否继续下一帧的信号——这正是raf(advance)里advance返回值的来源。Controller 在 packages/core/src/Controller.ts 用raf.onFrame(this._onFrame)注册逐帧钩子。滚动监听packages/shared/src/dom-events/scroll/index.ts 的onScroll创建常驻任务raf(animateScroll)listener 返回true保证持续运行滚动/缩放事件触发时通过各ScrollHandler.advance()计算信息清理时用raf.cancel(animateScroll)移除。测试环境packages/mock-raf/index.js 提供可手动step()/flush()的 mock 实现测试中通过raf.use(mockRaf.raf)注入见 packages/rafz/src/index.test.ts 的 beforeEach用于确定性验证帧循环的启停与任务重复调度行为。全局配置packages/shared/src/globals.ts 的Globals.assign会把requestAnimationFrame、batchedUpdates、frameLoop、onDemand、now等选项转发到 rafz 实例上构成 react-spring 面向外部的统一配置入口。使用建议与限制何时用raf需要逐帧推进 JS 状态如动画数值计算回调接收dt返回true可常驻。何时用raf.write需要写 DOM 等原生状态且希望每帧只执行一次、与其他写操作合并。读写在帧内的位置纪律README 引用 Paul Irish 的经典 gist 强调——write阶段之前任何时候都可以读布局/样式onFrame之后才可以写。遵循这一纪律可避免强制同步布局layout thrashing。取消语义raf.cancel只作用于raf系列 handlertimeout 用返回对象的cancel()。不要滥用raf.sync它会跳过调度直接执行仅用于确实需要同步的场合。raf.throttle适合把高频事件如 mousemove、scroll的处理器限流到每帧一次且取最后一次参数但要注意它不保留this绑定。体积意识包体积 700Bmingzip是它的设计卖点适合作为动画类库的公共依赖层。延伸阅读帧循环在 react-spring 动画管线中的上层用法packages/shared/src/FrameLoop.ts、packages/core/src/scheduleProps.ts队列与类型定义packages/rafz/src/index.ts、packages/rafz/src/types.ts测试佐证packages/rafz/src/index.test.ts、packages/core/src/FrameLoopDemand.test.ts、packages/mock-raf/index.js事件接入示例packages/shared/src/dom-events/scroll/index.ts设计背景可参考 README 中列出的 prior artfastdom把 DOM 读写分批与 framesyncPopmotion 的单循环调度器rafz 的update/write 分相思想与二者一脉相承。【免费下载链接】react-spring✌️ A spring physics based React animation library项目地址: https://gitcode.com/gh_mirrors/re/react-spring创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考