React Native Reanimated 之 useAnimatedReaction:用 Worklet 精准响应共享值变化
【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated
导读
useAnimatedReaction是 react-native-reanimated 提供的一个响应式 Hook,它允许你监听某个(或多个)共享值(Shared Value)的变化,并在变化发生时于 UI 线程上执行自定义的 Worklet 逻辑。它特别适合"拿共享值的当前值与上一次的值做比较"这类场景,是实现派生状态、联动动画、碰撞检测等逻辑的基础设施。读完本文,你将掌握prepare/react两个回调的职责划分、dependencies三种传法的差异,以及该 Hook 在底层如何通过 mapper 机制驱动 UI 线程执行。
一、核心思想:prepare 定义输入,react 负责响应
按照 version-2.x 文档 的定义,useAnimatedReaction允许你在某些共享值变化时执行特定操作。其关键设计是:
第一个 Worklet(
prepare)中出现的所有共享值,共同构成这个 Hook 的"输入集合"。每当其中任意一个共享值发生变化时,两个 Worklet 都会按顺序依次被触发。同时,第二个 Worklet(react)可以修改任意共享值,但唯独不能修改第一个 Worklet 中使用过的那些共享值。
这套"输入集合"机制在源码中体现得很明确。在 useAnimatedReactionCommon.ts 中,Hook 会把prepare的闭包(__closure)取出来作为 mapper 的输入:
const inputs = Object.values(prepare.__closure ?? {}); useAnimatedReactionBase(prepare, react, undefined, inputs);随后useAnimatedReactionBase内部通过startMapper(fun, inputs)将这些输入注册到底层的 mapper 注册表中,为输入集合里的每个共享值挂上监听器。这正是"所有共享值变化都会触发两个 Worklet"这一行为的实现来源(详见 mappers.ts 中的startMapper与注册逻辑 mappers.ts)。
二、参数详解
2.1prepare[Function] —— 数据准备与输入声明
prepare是一个 Worklet,用于为第二个参数准备数据。它同时定义了输入集合,即"在哪些共享值变化时本 Hook 会被调用"。每次输入变化,prepare都会被重新执行一次,其返回值会作为react的第一个参数传入。
在 useAnimatedReactionCommon.ts 中可以看到内部实际执行的顺序:
const fun = () => { 'worklet'; const input = prepare(); react(input, previous.value); previous.value = input; };即:先执行prepare拿到本次输入,再调用react(input, previous),最后把本次输入存入previous共享值,作为下一次回调的"上一次结果"。
2.2react[Function] —— 真正的响应动作
react是一个 Worklet,接收两个参数:
- 第一个参数:本次
prepare返回的数据; - 第二个参数:上一次
prepare的返回结果(首次调用时为null)。
它可以在里面执行任意动作,可以修改任何共享值,但唯独不能修改prepare中使用过的共享值。文档对此给出了明确警告:如果违反这一约定,会导致死循环(endless loop)并造成极高的 CPU 占用。
之所以会死循环,从源码可以清晰解释:prepare闭包中的共享值都会被注册为 mapper 输入并挂上监听器。如果在react里反过来修改这些输入共享值,就会再次触发监听器 → 再次执行prepare→ 再次修改……形成无限循环。这一点在新版文档 useAnimatedReaction.mdx 的 Remarks 一节中也专门用反例做了强调。
2.3dependencies[Array] —— 可选的重建依赖
dependencies是可选参数,其作用是:当包裹组件重新渲染(rerender)时,控制 Hook 是否用更新后的值重建两个 Worklet。这在"Worklet 内部使用了依赖组件状态的变量"时尤为重要。
原文档给出的三种传法及其语义如下:
| 传法 | 行为 |
|---|---|
undefined(跳过该参数) | Worklet 会在其函数体或闭包中捕获的任何值(来自外层作用域的变量)发生变化时被重建 |
空数组[] | Worklet 仅在其函数体发生变化时才被重建 |
数组[val1, val2, ..., valN] | Worklet 在其函数体变化或数组中任意值变化时被重建 |
这一行为在源码中同样有据可查。在 useAnimatedReactionCommon.ts 中:
if (dependencies === undefined) { dependencies = [ ...Object.values(prepare.__closure ?? {}), ...Object.values(react.__closure ?? {}), prepare.__workletHash, react.__workletHash, ]; } else { dependencies = [ ...dependencies, prepare.__workletHash, react.__workletHash, ]; }可以看到:不传依赖时,Hook 会自动收集两个 Worklet 的闭包值(__closure)和 worklet 哈希;显式传入依赖时,则把你给的数组与两个 worklet 哈希合并。最终这些依赖会作为useEffect的依赖数组,驱动 mapper 的重新注册。
2.4 平台差异:Native 与 Web 的依赖处理
值得说明的是,dependencies在不同平台的实现侧重点不同。从源码结构看,仓库针对不同平台拆分了实现:
- 原生实现useAnimatedReaction.native.ts:在开发模式下,如果显式传入了
dependencies,会通过logger.warn提示"依赖只应被用于 Web 实现"。因为原生端始终依赖 Babel 插件自动收集闭包,因此传入的依赖会被忽略,实际以undefined传入 base 函数,由闭包收集逻辑接管。 - Web 实现useAnimatedReaction.ts:在没有 Worklets Babel 插件的情况下,
prepare的__closure可能是空的,此时会回退使用用户显式传入的dependencies作为 mapper 输入:
if (!inputs.length && dependencies?.length) { // let web work without Worklets Babel plugin inputs = dependencies; }这一点与新版文档中"dependencies仅在 Web 端不使用 Babel 插件时相关"的说明(见 useAnimatedReaction.mdx)相互印证。此外,在测试/快照场景下,mock.ts 将useAnimatedReaction定义为NOOP,即 Jest 等环境中该 Hook 不执行任何实际操作。
三、完整示例
3.1 带 dependencies 的基础用法
原文档的示例展示了"依赖组件 state 的派生计算 + 防止重复写入"的典型组合:
const App = () => { const [state, setState] = useState(0); const sv1 = useSharedValue(0); const sv2 = useSharedValue(0); const derived = useAnimatedReaction( () => { return sv1.value * state; }, (result, previous) => { if (result !== previous) { sv2.value = result - 5; } }, dependencies ); //... return <></>; };这里prepare的返回值依赖了组件 state(state),因此必须通过dependencies让 Worklet 在 state 变化时重建;而react内部先比较result !== previous再写入sv2,可以避免无意义的重复赋值。
3.2 原生端常用形态:省略 dependencies
在实际的原生开发中,由于 Babel 插件会自动收集闭包,最常见的写法是直接省略第三个参数。原文档的第二个示例即为此形态:
const x = useSharedValue(0); const x2 = useSharedValue(0); const maxX2 = 80; useAnimatedReaction( () => { return x.value / 1.5; }, (data) => { // data holds what was returned from the first worklet's execution if (x2.value < maxX2) { x2.value = data; } } );该示例中prepare对x做了一次缩放计算,react拿到结果后在阈值maxX2内把它写入x2,实现了"限幅跟随"效果——这是动画中非常常见的派生联动模式。
3.3 反例:会造成死循环的写法
useAnimatedReaction( () => { return width.value; // width 是 prepare 的输入 }, (currentValue) => { width.value += currentValue; // 🚨 在 react 里修改了输入共享值 → 无限循环 } );react内修改了prepare输入集合中的width.value,会再次触发监听器、再次运行prepare,从而陷入无限循环并导致高 CPU 占用,务必避免。
四、底层原理:从 Hook 到 UI 线程的 mapper 机制
要真正理解useAnimatedReaction,需要往下看一层:它最终借助的是 Reanimated 的 mapper(映射器)机制。
在 useAnimatedReactionCommon.ts 中,Hook 在useEffect内调用startMapper(fun, inputs),并在清理函数中调用stopMapper(mapperId):
useEffect(() => { const fun = () => { 'worklet'; const input = prepare(); react(input, previous.value); previous.value = input; }; const mapperId = startMapper(fun, inputs); return () => { stopMapper(mapperId); }; }, dependencies);而 mappers.ts 中的startMapper会:
- 分配全局自增的
mapperID(从 9999 开始递增); - 通过
scheduleOnUI把注册动作调度到 UI 线程; - 在 UI 线程的 mapper 注册表中调用
registry.start(mapperID, worklet, inputs, outputs)。
registry.start(见 mappers.ts)会递归地extractInputs从输入中提取出真正的共享值对象,然后对每个共享值调用sv.addListener(mapper.id, ...)挂上脏标记监听器;一旦某个输入共享值变化,监听器被触发,mapper 会被标记为 dirty 并请求下一帧更新(maybeRequestUpdates)。组件卸载时,stopMapper则把这些监听器逐一移除。
这条链路解释了为什么useAnimatedReaction的两个回调始终运行在 UI 线程上、为什么只有"输入集合"中的共享值能触发执行,也解释了为什么它天然适合与useAnimatedStyle、withTiming等动画 API 组合——因为整套响应都是基于 UI 线程的共享值监听,不经过 JS 线程的 React 渲染周期。
五、使用建议与注意事项
结合 version-2.x 文档 与新版 useAnimatedReaction.mdx 的 Remarks,总结如下:
- 不要在
react中修改prepare使用过的共享值,否则会无限循环、CPU 飙升; - 传入
prepare/react的回调会被自动 worklet 化(workletized),运行在 UI 线程上,因此其中不应包含只能在 JS 线程执行的同步阻塞逻辑; - 从类型上看,
react的第二个参数(上一次prepare的结果)首次为null,做比较时需注意可空类型处理; - 虽然技术上讲可以用它响应任意有状态的 React 值,但如果你只是想响应组件状态变化,更合适的工具是普通的
useEffect——useAnimatedReaction的价值在于共享值驱动的 UI 线程联动; - 平台兼容性方面,该 Hook 在 Android、iOS 与 Web 上均可用(见 useAnimatedReaction.mdx 的 PlatformCompatibility 声明),但
dependencies参数在原生端会被忽略并触发开发告警,其实际意义集中在无 Babel 插件的 Web 场景。
结语
useAnimatedReaction把"监听共享值变化"这件事从 React 生命周期中剥离出来,交给 UI 线程上的 mapper 机制去完成:prepare声明输入并产出数据,react拿新旧值做比较与响应。理解其prepare/react/dependencies三者分工,并牢记"不要反向修改输入共享值"这条红线,你就能在动画联动、派生状态同步等场景中安全、高效地使用它。如果想深入了解实现细节,可以直接阅读 useAnimatedReactionCommon.ts、useAnimatedReaction.native.ts、useAnimatedReaction.ts 与 mappers.ts 四处源码。
【免费下载链接】react-native-reanimatedReact Native's Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考