
React Native Reanimated 2.x 核心概念精讲Shared Values 的跨线程数据共享、响应式更新与动画驱动机制【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated导读本文以 React Native Reanimated 2.x 版本官方文档中关于 Shared Values 的章节为骨架系统讲解这一库中承载可动画数据的核心原语如何用它跨线程安全地读写数据、如何借助useAnimatedStyle/useDerivedValue建立响应式依赖、又如何通过withTiming/withSpring驱动并随时打断动画。读完本文你将掌握 Shared Values 的完整 API 用法、线程模型下的同步/异步语义以及它与 React Native 原生Animated.Value的逐项差异能够直接写出可复用的动画与手势交互代码。Shared Values 的定位Reanimated 2.x 的共享内存原语Shared Values共享值是 Reanimated 2.0 的基石概念之一。如果你熟悉 React Native 官方的 Animated API。携带数据跨线程读写的核心机制通过.value属性读写共享数据Shared Value 对象本质上是共享数据的引用对数据的读写必须通过它的.value属性完成。这是 Reanimated 2.x 新手最容易踩的坑误以为拿到 Shared Value 引用就等于拿到数据本身从而直接使用引用而不是访问.value。const sharedVal useSharedValue(3.1415); // 读取 console.log(sharedVal.value); // 写入 sharedVal.value 42;从源码结构看SharedValue接口正是围绕.value设计的。仓库中的类型定义见 commonTypes.ts除了.value之外还暴露了get()、set()、addListener、removeListener、modify等底层方法而日常开发中 99% 的场景只需使用.value。线程模型UI 线程同步、JS 线程异步为了在两套 JS 上下文间提供安全快速的共享访问Shared Values 在设计上做了权衡UI 线程worklet 内读写是同步的动画执行期间绝大多数更新发生在 UI 线程因此从 worklet 中更新.value后紧接着的读取立即可见无需等待。React Native JS 线程更新是异步的从 JS 线程写入.value时Reanimated 内核并不会立即执行更新而是把更新调度到 UI 线程执行以避免并发冲突。可以把它理解成 React 的 state——提交了更新但不会立刻生效读取最新值需要等待下一轮渲染。这一设计在仓库源码中有直接对应物。原生实现见 mutables.native.ts当运行环境为 React Native 线程时写入走scheduleOnUI把任务调度到 UI 线程而从 UI 线程读取则通过getSync同步拉取。Web / Jest 环境的轻量实现则见 mutables.ts它用 getter/setter 拦截.value访问并在每次更新时通知监听器。创建与更新useSharedValue与两种写入方式创建 Shared Value 使用useSharedValue钩子其唯一参数是初始值可以是数字、字符串、布尔值、对象、数组等任意原始类型或嵌套数据结构import { useSharedValue } from react-native-reanimated; function SomeComponent() { const sharedVal useSharedValue(0); return ( Button onPress{() (sharedVal.value Math.random())} titleRandomize / ); }上面的更新发生在 React Native JS 线程属于异步更新。若在 worklet 内更新则是同步的例如在滚动事件 worklet 中记录滚动偏移import Animated, { useSharedValue, useAnimatedScrollHandler, } from react-native-reanimated; function SomeComponent({ children }) { const scrollOffset useSharedValue(0); const scrollHandler useAnimatedScrollHandler({ onScroll: (event) { scrollOffset.value event.contentOffset.y; }, }); return ( Animated.ScrollView onScroll{scrollHandler} {children} /Animated.ScrollView ); }上述useAnimatedScrollHandler中的onScroll会被编译为 worklet 并在 UI 线程执行因此对scrollOffset的写入是同步的、逐帧跟随手势。源码视角钩子的生命周期细节从实现看useSharedValue.ts 有两个值得注意的细节初始值惰性计算它借助useState的惰性初始化只会在组件首次挂载时调用一次若传入的是函数则执行该函数得到初始值适合昂贵计算。仓库注释明确警告不要把函数包括 worklet存入 Shared Value。卸载时自动取消动画useEffect的清理函数中调用cancelAnimation(mutable)保证组件卸载时清理仍在进行的动画避免内存泄漏。仓库测试 SharedValue.test.tsx 验证了 Shared Value 在 Jest 环境下的基础行为初始值正确渲染、JS 线程内的异步更新不会导致组件文本立即变化hooks.useSharedValue.test.tsx 则验证了 Shared Value 在组件重渲染时保留原值不会因 rerender 被重置为初始值。响应式Shared Values 驱动 UI 自动更新Shared Values 的第二大职责是提供**响应式reactiveness**能力共享值一旦更新就能触发 UI 线程上对应代码的执行进而启动动画、刷新视图等。这套响应式层对开发者完全透明其底层概念是mapper worklet——即捕获闭包引用了某个 Shared Value 的响应式 worklet。目前创建响应式 worklet 有两种官方途径分别对应两个钩子useAnimatedStyle捕获 Shared Value返回值作为组件的动画样式useDerivedValue捕获一个或多个 Shared Value返回值成为新的只读Shared Value。当一个 Shared Value 被这些钩子提供的 worklet 捕获后该值一旦变化worklet 就会重新执行。Reanimated 引擎会在内部为 Shared Value 与响应式 worklet 之间构建一张依赖图从而做到两点只执行需要更新的代码——例如存在共享值x、派生值y依赖x以及同时使用x、y的动画样式时x更新只会重跑派生值y的 worklet而不会盲目重跑所有 worklet保证更新顺序正确——因为样式依赖y引擎会保证y的更新器先于样式更新器执行。响应式示例点击移动方块import Animated, { useSharedValue, useAnimatedStyle, } from react-native-reanimated; function Box() { const offset useSharedValue(0); const animatedStyles useAnimatedStyle(() { return { transform: [{ translateX: offset.value }], }; }); return ( Animated.View style{[styles.box, animatedStyles]} / Button onPress{() (offset.value Math.random() * 255)} titleMove / / ); }这里offset被useAnimatedStyle的 worklet 捕获。初始值为 0每次点击按钮offset被更新为 01 的随机数引擎随即重跑样式 worklet把offset.value * 255映射到translateX视图立即跳到新的 x 位置源码视角mapper 的注册与调度依赖图 按序重跑在原生代码中由 mapper 机制实现。mappers.native.ts 中的startMapper会把 worklet 连同它的输入被捕获的 Shared Value 集合注册到 UI 线程的全局__mapperRegistry中之后由引擎在每一帧统一调度输入值变化时找出受影响的 mapper、按依赖顺序执行、并跳过输入未变化的部分。这也解释了为什么文档强调worklet 除了首次运行外只会在值更新时执行。结合useDerivedValue还可以构造派生共享值链useDerivedValue.ts 通过读取 worklet 的__closure闭包捕获列表自动收集它依赖的 Shared Value 作为 mapper 输入Web 端在没有 Babel 插件时则退化为使用显式传入的dependencies数组。派生值用法的完整示例可参考 useDerivedValue 文档 中的progress → width progress.value * 250演示。驱动动画把目标值包装成动画动画在 Reanimated 2.x 中是一等公民。启动动画最直接的方式就是把 Shared Value 的目标值用动画工具函数包裹起来再赋给.value例如withTiming或withSpringimport { withTiming } from react-native-reanimated; someSharedValue.value withTiming(50);此时 Shared Value 不再是瞬间跳到 50而是从当前值开始、按时间曲线过渡到 50。这种启动方式在 UI 线程与 React Native JS 线程都可以使用。把上一节的例子稍加改动用withSpring包裹随机值位移就变成了平滑的弹性过渡import Animated, { withSpring } from react-native-reanimated; function Box() { const offset useSharedValue(0); const animatedStyles useAnimatedStyle(() { return { transform: [{ translateX: offset.value }], }; }); return ( Animated.View style{[styles.box, animatedStyles]} / Button onPress{() { offset.value withSpring(Math.random() * 255); }} titleMove / / ); }关于动画自定义与完成回调如withTiming的duration/easing配置、withSpring的弹簧参数可查阅 withTiming 与 withSpring 的 API 文档。源码视角动画是如何跑起来的从源码看赋动画值的过程由 valueSetter.ts 这个 worklet 统一处理核心流程为中断旧动画如果该 Shared Value 上已有未完成的动画先标记cancelled并以false触发其回调识别动画对象通过onFrame/onStart属性或函数类型判断新值是否为动画定义启动帧循环调用animation.onStart初始化然后用requestAnimationFrame循环执行onFrame逐帧把animation.current写回mutable._value直到onFrame返回true表示完成。withTiming的具体实现见 timing.ts默认duration为 300ms、默认缓动为Easing.inOut(Easing.quad)逐帧计算progress easing(runtime / duration)并插值得到当前值。withSpring的实现见 spring.ts默认使用GentleSpringConfig并支持duration/dampingRatio等参数。读取动画进度动画启动后.value会与动画进度保持同步若初始值为 0、用withTiming(50)执行 300ms 的动画那么在动画进行中每次读取.value都会返回 050 之间的某个数对应当前时刻动画插值的位置。因此随时读取.value即可拿到动画中间态无需额外监听。中断动画让动画无痛切换Shared Values 保存了动画运行的状态因此 Reanimated 的所有动画都是**完全可中断interruptible**的若动画进行中给.value赋一个普通值数字等静态值旧动画会被立即取消新值即刻生效不会产生突然跳变若赋的值是另一个动画如withSpring旧动画会平滑过渡到新动画且速度velocity等动画参数会传递给新动画——这一点对弹簧动画尤为关键能实现从一个动画无缝衔接到另一个动画的丝滑体验。下面的动图演示了连续快速点击按钮时与上一个示例代码完全相同仅点击更频繁新动画在前一个动画尚未结束时启动的效果这套中断逻辑同样由 valueSetter.ts 保证写入新值时把previousAnimation置空并标记cancelled随后新动画以previousAnimation作为参考启动onStart的第四个参数从而继承速度等动态属性。取消动画如果只想停止当前动画而不启动新动画使用cancelAnimationimport { cancelAnimation } from react-native-reanimated; cancelAnimation(someSharedValue);cancelAnimation在 UI 线程与 React Native JS 线程均可调用。其实现非常巧妙见 util.native.ts它通过sharedValue.value sharedValue.value的自我赋值触发valueSetter从而走一遍取消旧动画的分支从 JS 线程调用时则通过scheduleOnUI调度到 UI 线程执行。速查对照Shared Values 与 Animated.Value下表面向熟悉AnimatedAPI 的开发者逐项对比两者差异可作为迁移时的速查参考此表无意评判优劣仅作对照维度Animated ValueShared Value携带的数据仅支持数字和字符串任意原始类型或嵌套数据结构对象、数组、字符串、数字、布尔值与视图属性连接直接把Animated.Value作为 prop 传入不能直接作为视图 prop需通过useAnimatedStyle/useAnimatedProps读取共享值并计算样式/props更新值value.setValue(...)使用原生驱动时为异步调用更新.value属性UI 线程同步React Native JS 线程异步读取值通过value.addListener注册监听器获取更新直接读.value属性UI 线程与 JS 线程均可读取当前值运行动画Animated.spring/Animated.timing等传入 Animated Value 后调用.start()照常更新.value把目标值用动画工具函数如withTiming包裹停止动画持有Animated.timing等返回的动画对象引用调用其stopAnimation()调用cancelAnimation并传入正在运行动画的 Shared Value插值使用 Animated Value 的成员方法interpolate()使用接收数字与配置并返回插值结果的interpolate函数若需要自动跟踪另一 Shared Value 插值的共享值可配合useDerivedValue最佳实践小结统一走.value无论读写永远通过.value属性不要试图直接使用 Shared Value 引用作为数据。牢记线程语义workletUI 线程内更新是同步的React Native JS 线程更新是异步的跨线程共享数据时优先考虑在 worklet 内完成读写。动画即值把withTiming/withSpring包装后的动画对象赋给.value即可启动动画且天然支持进度读取与中途打断停止动画用cancelAnimation。依赖图自动维护useAnimatedStyle与useDerivedValue会自动捕获依赖的 Shared Values 并构建映射关系开发者只需关注值变了之后样式/派生值该怎么算顺序与去重交给引擎。生命周期安全useSharedValue会在组件卸载时自动取消动画不要向 Shared Value 中存放函数也不要依赖 JS 线程的立即读取。文中涉及的源码与测试均可继续深入阅读useSharedValue.ts、mutables.native.ts、valueSetter.ts、mappers.native.ts、SharedValue.test.tsx 等文件可作为理解底层实现的入口。【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考