ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

React Native Reanimated 自定义事件处理器实战:用 useHandler 与 useEvent 构建专属动画事件 Hook

2026/9/15 23:56:28 拓冰建站 浏览量
React Native Reanimated 自定义事件处理器实战:用 useHandler 与 useEvent 构建专属动画事件 Hook React Native Reanimated 自定义事件处理器实战用 useHandler 与 useEvent 构建专属动画事件 Hook【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated导读Reanimated 2.x 除了内置的手势与滚动事件处理器之外还向开发者暴露了一套底层 APIuseHandler与useEvent用于为任意自定义动画组件创建专属的事件处理 Hook例如为PagerView这类第三方分页组件编写useAnimatedPagerScrollHandler。读完本文你将掌握自定义事件处理器的完整套路并能复刻出与官方useAnimatedScrollHandler结构一致的、在 UI 线程上运行的动画事件 Hook。本文对应的原始文档为 version-2.x/fundamentals/custom_events.md其中讲解的useHandler/useEvent底层能力在当前仓库源码中依然完整保留下文会结合源码逐层拆解。一、为什么需要自定义事件处理器Reanimated 的动画逻辑默认运行在 UI 线程的工作集worklet中。官方内置的useAnimatedScrollHandler、useAnimatedGestureHandler2.x只能覆盖 ScrollView 与手势库事件。当第三方组件如分页器、视频播放器、可拖拽列表向外派发自有原生事件时我们无法直接把处理逻辑写进 worklet。useHandler与useEvent组成的底层 API 就是为了解决这一缺口它们允许我们把任意原生事件桥接到 UI 线程并以和官方 Hook 完全一致的形态handlers对象 dependencies数组封装出专属的useAnimatedXxxHandler。二、典型场景为 Pager 组件编写滚动处理器原文档以react-native-pager-viewcallstack 出品的跨平台分页组件为例需求是根据分页器的滚动进度驱动动画分页指示点pagination dots。1. 用 createAnimatedComponent 包装组件要让第三方组件接入 Reanimated 事件体系第一步是用Animated.createAnimatedComponent将其包装为动画组件const AnimatedPagerView Animated.createAnimatedComponent(PagerView); const PagerExample () { const scrollPosition useSharedValue(0); return ( AnimatedPagerView initialPage{0} onPageScroll{scrollHandler} View collapsable{false} Text{Page ${1}}/Text /View View collapsable{false} Text{Page ${2}}/Text /View /AnimatedPagerView ); };这里scrollPosition是一个共享值shared value用来持续保存分页器的当前滚动量scrollHandler则是由下一步自定义 Hook 返回的、可挂在onPageScroll上的事件处理器。注意View上的collapsable{false}在 Android 上被折叠collapsable的视图不会产生原生视图句柄事件可能无法正确挂载。2. 封装专属 HookuseAnimatedPagerScrollHandler接着定义自定义 Hook其内部的处理函数会以 worklet 形式在 UI 线程执行const scrollHandler useAnimatedPagerScrollHandler({ onPageScroll: (e) { worklet; scrollPosition.value e.offset e.position; }, });在onPageScrollworklet 中事件对象e携带了分页器当前的position当前页索引与offset页内偏移二者相加即得到精确的滚动位置。这个值既能直接驱动动画也能用来计算内容相对分页器的偏移量。3. 用 useHandler useEvent 实现自定义 Hook自定义 Hook 的完整骨架如下原文档核心代码function useAnimatedPagerScrollHandler(handlers, dependencies) { const { context, doDependenciesDiffer } useHandler(handlers, dependencies); return useEvent( (event) { worklet; const { onPageScroll } handlers; if (onPageScroll event.eventName.endsWith(onPageScroll)) { onPageScroll(event, context); } }, [onPageScroll], doDependenciesDiffer ); }这段代码只做了三件事通过useHandler(handlers, dependencies)获得一个跨事件持久的context对象以及处理器是否需要重建的标志doDependenciesDiffer调用useEvent(worklet, eventNames, rebuild)生成一个可在 UI 线程处理原生事件的 worklet 事件处理器在 worklet 内按event.eventName分发到用户传入的onPageScroll并把context一并透传。三、深入 useHandlercontext 与依赖比较的底层实现useHandler的职责在原文档中被概括为提供 context 对象并告知是否需要重建其在当前仓库中的实现src/hook/useHandler.native.ts 与 src/hook/useHandler.ts验证了这一点context 的惰性创建context是一个 getter首次访问时才通过makeShareable({})创建可分享的普通对象并缓存于stateRef。因此它可以跨多次事件触发持久保存状态多个 worklet 之间也能共享同一个 context 互相通信。doDependenciesDiffer 的判定当检测到 Babel 插件可用isBabelPluginEnabled检查 handlers 中是否出现 worklet 函数时通过areWorkletHandlersEqual比较前后两次 handlers 的__workletHash与闭包__closure是否一致见 src/hook/useHandlerCommon.ts未启用 Babel 插件如纯 Web 环境时则退化为按dependencies数组逐项用Object.is比较。并发渲染安全对prevHandlers/prevDependencies的写入放在useEffectcommit 之后中避免并发模式下被中断的渲染污染状态。开发期校验__DEV__下调用ensureWorkletHandlers若传入的 handlers 中有非 worklet 函数会直接抛出错误Only worklet functions are allowed。参数说明参数类型说明handlersGeneralHandlersEvent, Context键与原生事件名对应的 worklet 集合每个 worklet 接收(event, context)dependenciesDependencyList可选依赖数组仅在 Web 且未启用 Babel 插件时有意义原生端传入会触发警告返回值为UseHandlerContextContext包含context事件间共享状态与doDependenciesDiffer是否应重建事件处理器。四、深入 useEventworklet 事件处理器的生成useEvent的签名与实现位于 src/hook/useEvent.tsfunction useEventEvent extends object, Context extends Recordstring, unknown never( handler: EventHandlerEvent, Context, // 处理事件的 worklet eventNames?: readonly string[], // 要订阅的原生事件名数组 rebuild?: boolean // 是否需要重建处理器 ): EventHandlerProcessedEvent, Context;参数语义handler一个 worklet收到事件对象后执行event是ReanimatedEventEvent其载荷因事件类型而异。eventNames需要监听的去掉了on前缀的原生事件名数组如[onPageScroll]、[onScroll]。它会决定该处理器被挂到哪些原生事件上。rebuild是否用新的 handler / eventNames 重建内部处理器通常直接透传useHandler返回的doDependenciesDiffer。实现要点来自源码useEvent内部用useRef缓存一个WorkletEventHandler实例首次调用时通过new WorkletEventHandler(handler, eventNames)创建src/WorkletEventHandler.ts。当rebuild为true时调用workletEventHandler.updateEventHandler(handler, eventNames)原地更新 worklet 与订阅事件名而不是新建实例从而避免组件重渲染时反复注册/注销事件。在 Web 实现中WorkletEventHandler会为每个eventName生成一个 JS 监听器把evt.nativeEvent展开并附上eventName字段后转交 workletjsListener函数这解释了为何 worklet 内可以直接读取event.eventName。useEvent的返回值在类型层面伪装成普通函数EventHandlerProcessed实际内部携带workletEventHandler由createAnimatedComponent识别后完成事件挂载。五、官方参照实现useAnimatedScrollHandler 的完整套路官方内置的 useAnimatedScrollHandler 正是useHandler useEvent组合的范本值得逐行对照。它的实现展示了自定义 Hook 的几个通用工程细节// 支持传入单个函数或 handlers 对象 const scrollHandlers typeof handlers function ? { onScroll: handlers } : handlers; const { context, doDependenciesDiffer } useHandler(scrollHandlers, dependencies); // 按需构建事件订阅数组 const subscribeForEvents [onScroll]; if (scrollHandlers.onBeginDrag ! undefined) subscribeForEvents.push(onScrollBeginDrag); if (scrollHandlers.onEndDrag ! undefined) subscribeForEvents.push(onScrollEndDrag); if (scrollHandlers.onMomentumBegin ! undefined) subscribeForEvents.push(onMomentumScrollBegin); if (scrollHandlers.onMomentumEnd ! undefined) subscribeForEvents.push(onMomentumScrollEnd); return useEvent((event) { worklet; const { onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd } scrollHandlers; if (onScroll event.eventName.endsWith(onScroll)) onScroll(event, context); else if (onBeginDrag event.eventName.endsWith(onScrollBeginDrag)) onBeginDrag(event, context); // ...其余事件分支 }, subscribeForEvents, doDependenciesDiffer);从这段代码可以提炼出自定义 Hook 的三条最佳实践事件名映射worklet 内用event.eventName.endsWith(...)做匹配注意原生事件名与用户侧回调名的差异例如回调onBeginDrag对应原生事件onScrollBeginDrag把订阅哪些事件集中在subscribeForEvents数组中管理。单函数语法糖允许用户直接传一个函数作为onScroll降低使用门槛。context 透传所有分支都携带context保证状态在所有事件类型间共享。六、事件挂载链路从 Hook 到原生视图自定义 Hook 返回的处理器是如何最终到达原生组件的事件系统的答案藏在createAnimatedComponent的 NativeEventsManager.ts 中组件挂载时attachEvents()遍历 props找出所有携带workletEventHandler即WorkletEventHandler实例的 prop调用handler.registerForEvents(viewTag, propKey)把事件注册到对应原生视图。组件更新时updateEvents(prevProps)会比较新旧 handler 的引用若视图 tag 变化则整体重挂否则只对发生替换的处理器重新注册实现精细化的增量更新。组件卸载时detachEvents()统一调用unregisterFromEvents(viewTag)清理避免泄漏。这一链路意味着只要某个 prop 的值是useEvent的返回值Reanimated 就会自动把它当作原生事件处理器进行挂载与生命周期管理这正是自定义事件 Hook 能够无缝工作的根基。相关判定逻辑见 PropsFilter.tsx。七、完整示例与实战要点将以上片段组装为可直接运行的最小分页器示例import Animated, { useSharedValue, useHandler, useEvent } from react-native-reanimated; import PagerView from react-native-pager-view; const AnimatedPagerView Animated.createAnimatedComponent(PagerView); function useAnimatedPagerScrollHandler(handlers, dependencies) { const { context, doDependenciesDiffer } useHandler(handlers, dependencies); return useEvent( (event) { worklet; const { onPageScroll } handlers; if (onPageScroll event.eventName.endsWith(onPageScroll)) { onPageScroll(event, context); } }, [onPageScroll], doDependenciesDiffer ); } const PagerExample () { const scrollPosition useSharedValue(0); const scrollHandler useAnimatedPagerScrollHandler({ onPageScroll: (e) { worklet; scrollPosition.value e.offset e.position; // 驱动指示点动画 }, }); return ( AnimatedPagerView initialPage{0} onPageScroll{scrollHandler} View collapsable{false} Text{Page ${1}}/Text /View View collapsable{false} Text{Page ${2}}/Text /View /AnimatedPagerView ); };实战要点worklet 标记不可省略所有会在 UI 线程执行的函数体首行都要写worklet;否则 Babel 插件无法将其序列化到 UI 线程开发模式下useHandler的ensureWorkletHandlers校验也会直接抛错。事件名大小写与前后缀event.eventName通常是 React Native 原生事件名如topPageScroll类后缀被归一化后的形态统一用endsWith(onPageScroll)这种后缀匹配最稳妥可参考官方useAnimatedScrollHandler的写法。dependencies 的作用范围在原生端dependencies参数基本无用传入会在__DEV__下触发警告依赖比较由 worklet 的 hash 与闭包自动完成只有 Web 且未启用 Babel 插件时才真正依赖该数组。一个处理器可服务多个组件useEvent返回的处理器可以同时传给多个动画组件每个组件都会收到自己对应的事件见新版文档 useEvent 的 Returns 说明。Web 平台限制并非所有原生事件在 Web 上都可用滚动类事件通常只有onScroll跨浏览器稳定支持跨端使用前应在目标平台验证新版文档 useEvent 的 Remarks 中有明确提示。八、结语useHandler与useEvent是 Reanimated 自定义动画事件体系的基石useHandler负责状态context与重建决策useEvent负责把 worklet 包装成可挂载的原生事件处理器而createAnimatedComponent则负责在组件生命周期内自动注册与清理。理解这三者的协作就能像官方实现useAnimatedScrollHandler源码参照一样为任何派发原生事件的第三方组件量身定制 UI 线程级的事件 Hook。若需将多个此类 Hook 合并使用还可进一步了解 useComposedEventHandler基于useEvent组合多个事件处理器的官方 Hook。【免费下载链接】react-native-reanimatedReact Natives Animated library reimplemented项目地址: https://gitcode.com/GitHub_Trending/re/react-native-reanimated创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考