ARTICLE DETAIL

建站实战干货

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

Radix Primitives `@radix-ui/react-presence` 演进全解:动画进出场原理解析与 1.1.4~1.1.10 版本变更深度剖析

2026/10/3 17:32:29 拓冰建站 浏览量
Radix Primitives `@radix-ui/react-presence` 演进全解:动画进出场原理解析与 1.1.4~1.1.10 版本变更深度剖析 前端UI组件【免费下载链接】primitivesRadix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by workos.项目地址https://gitcode.com/gh_mirrors/pr/primitives点击查看免费下载radix-ui/react-presence是 Radix Primitives 内部承担元素存在性管理的核心工具组件当present属性切换时它通过读取元素 computed style 中的animation-name判断进出场动画让延迟卸载与动画完成后再卸载成为可能。本文以本仓库 packages/react/presence/CHANGELOG.md 记载的 1.1.41.1.10 版本演进为主线逐条还原每个变更背后的源码机制、问题根因与性能/兼容性考量帮助你在理解 Radix 体系Dialog、Popover、Menu、Toast 等均依赖它的同时掌握一套可在自己组件库中复用的进出场动画 存在性状态机工程方案。先看懂 Presence 是什么一个内部存在的工具在阅读版本历史之前先明确这个包在仓库中的定位。官方说明非常直白——packages/react/presence/README.md 写道This is an internal utility, not intended for public usage.它并非面向终端用户的 API 组件而是被上层组件广泛引用的内部依赖。仓库内大量组件包都声明了对它的依赖例如 checkbox.tsx、collapsible.tsx、dialog.tsx、hover-card.tsx、menu.tsx、popover.tsx、toast.tsx 等。以 dialog.tsx 为例实际用法是Presence present{forceMount || context.open} {/* 内容部分 */} /Presence即当 Dialog 打开/关闭状态变化时Presence负责决定子树是立刻挂载、立刻卸载还是挂起卸载等待退出动画播完。理解这一点后续所有版本变更的意义就清晰了这个包的任何改动都会级联影响几乎全部 Radix 浮层类组件的行为、性能与 React 兼容性。核心原理三段式状态机驱动的存在性判定presence.tsx 中Presence组件本身很薄它从usePresence(present)拿到isPresent与ref然后决定渲染方式若children是函数则强制挂载forceMount把{ present: isPresent }传回给调用方由调用方自己决定渲染细节否则取唯一子元素合并ref后克隆仅在isPresent为真时返回该元素否则返回null。真正的核心在 presence.tsx 中定义的有限状态机。其状态与转移关系为当前状态事件下一状态语义mountedUNMOUNTunmounted无退出动画直接卸载mountedANIMATION_OUTunmountSuspended检测到退出动画开始挂起卸载unmountSuspendedMOUNTmounted动画期间再次进入恢复挂载unmountSuspendedANIMATION_ENDunmounted动画结束真正卸载unmountedMOUNTmounted重新挂载最终isPresent state mounted || state unmountSuspended即只要还处在退出动画播放期unmountSuspended元素就仍然存在于 DOM 中。这套状态机由 use-state-machine.tsx 中的useStateMachine实现底层只是用React.useReducer按事件查表转移nextState machine[state][event] ?? state。类型层面通过UnionToIntersection技巧从状态描述类型推导出合法事件集合保证状态定义即类型约束。关键判定如何知道退出动画开始了浏览器并没有在动画开始瞬间就触发的事件——animationstart会在animation-delay耗尽后才触发对退出动画而言往往太晚。因此 Radix 采用的方案是比较计算样式中的animation-name是否发生变化见 presence.tsx 的注释present从true变false时若当前animation-name为none或display为none说明根本没有退出动画立即UNMOUNT否则对比上一次记录的 animation-name与当前 animation-name若确实不同说明新动画已开始进入ANIMATION_OUT反之UNMOUNT。随后在animationend/animationcancel事件上监听presence.tsx确认动画真正结束后才发送ANIMATION_END。这里还有一个细节事件回调中用CSS.escape(event.animationName)与计算样式对比以正确处理 keyframe 名称中含转义字符的情况这正是 1.1.5 修复的内容见下文。防止动画结束后内容闪一下presence.tsx 的注释记录了一个微妙问题React 18 并发模式下ANIMATION_END对应的更新会在动画结束后一帧才应用导致卸载瞬间内容闪现flash。修复方式是当节点在退出动画期间时把node.style.animationFillMode临时置为forwards让节点保持在最后一帧关键帧样式随后用setTimeout在节点有足够时间卸载后再还原 fill mode。注释中还提到旧实现曾用ReactDOM.flushSync强制同步刷新但会导致节点在合成animationEnd事件派发前就被移出 DOM使用户自带的动画结束回调不被触发因此被弃用对应 radix-ui/primitives#1849。版本演进全解读从 1.1.4 到 1.1.10以下逐条解读 CHANGELOG 中记录的每一次发布。1.1.4修复内存泄漏Fix memory leak in PresenceCHANGELOG 没有展开细节但结合源码可以推断泄漏点所在。在 presence.tsx 的useLayoutEffect中每当node变化都会在节点上注册animationstart/animationcancel/animationend三个事件监听器并在清理函数中removeEventListener与ownerWindow.clearTimeout(timeoutId)。1.1.4 修复的目标正是确保节点被移除、组件卸载或node引用变化时这些监听器与挂起的setTimeout定时器被可靠清理避免旧节点长期滞留于内存。1.1.5animationend处理转义字符Ensured that theanimationendevent is handled correctly when the keyframe has escapable characters (#2763)如前所述事件对象中的event.animationName是未转义的 CSS 语法形式而getComputedStyle返回的animationName是转义后的形式两者直接比较会失败。修复即在 presence.tsx 中改用CSS.escape(event.animationName)后再做includes包含判断。如果你的动画 keyframe 名称中包含特殊字符空格、连字符前缀、数字开头等这一行就是正确判定当前动画是否已结束的关键。1.1.6修复 React 19 下的 Maximum update depth exceeded 无限循环Fixed a Maximum update depth exceeded infinite loop in React 19 that could occur whenPresencewas given a child with an unstable ref.这是整个版本历史中技术含量最高的一次修复。问题根因记录在 presence.tsx 的注释中React 19 中如果回调 ref 的身份identity在两次渲染之间改变React 会在每次 commit 时先以null分离旧 ref、再附加新 ref。Presence自身的 ref 会调用setNode(node)触发一次更新而若传入子元素的消费者 ref 身份不稳定例如内联箭头函数该 ref 的分离/附加就会反复触发setNode更新最终形成最大更新深度超限死循环。解决方案是新增useStableComposedRefs与常规的useComposedRefs不同它保证返回的 ref 回调身份永远不变React.useCallback(..., [])同时把最新一组 refs 存入refsRef.current在 attach/detach 时统一读取。由于Presence的 ref 不再因消费者 ref 变化而重建React 19 不会反复分离/附加它循环被打破而最新消费者 ref 仍能在挂载/卸载时正确收到节点。presence.test.tsx 为此提供了回归测试使用每次渲染都新建、且在 attach 时触发重渲染的内联回调 ref断言渲染循环次数小于 25 且不抛异常。同文件还验证了稳定 ref 场景与子节点 ref 能正确收到 DOM 节点的转发行为。同一版本还顺带补充了repository.directory到各package.json即本包 package.json 中的directory: packages/react/presence便于工具链在 monorepo 中定位源码目录。1.1.7性能优化——减少 FocusScope 与 Presence 中的强制 reflowImproved performance by reducing forced reflow inFocusScopeandPresence“强制 reflow”forced reflow / forced synchronous layout指在浏览器尚未完成上一次样式计算时就同步读取布局相关属性迫使浏览器立即进行昂贵的同步样式重算。结合 presence.tsx 的当前实现可以还原当时的优化思路在useLayoutEffect布局阶段中当present变为true时立刻读取还算干净的animation-name存入mountAnimationNameRef到后续的 passiveuseEffect中React 兄弟 effect如react-remove-scroll、DismissableLayer等可能已经弄脏了 body 样式此时不再重新读取实时 CSSStyleDeclaration而是直接消费布局阶段缓存的动画名对应注释中引用的 radix-ui/primitives#1634同理ref 回调在 commit 阶段触发、早于任何 passive effect因此在 ref 回调中“提前”缓存mountAnimationNameRef.current让后续 passive effect 跳过冗余的实时样式读取。整体策略就是一句话把昂贵的 getComputedStyle 读取尽可能前移到样式干净的时机并缓存避免在样式已被污染的时机重复读取。这正是优化性能的典型手法也解释了为何 presence.tsx 中同一个animation-name会在 ref 回调、布局阶段、passive 阶段三处流转缓存。1.1.8改善 tree-shaking让打包器能丢弃未使用组件Improved tree-shaking so bundlers can drop unused components. Component parts are now marked/* __PURE__ */and use named render functions instead ofComponent.displayName ...assignments, which previously prevented dead-code elimination with some bundlers.打包优化层面的两个具体动作各组件部件改用具名渲染函数named render functions而非Component.displayName ...赋值——后者在部分打包器如某些 webpack 配置下的 Rollup 依赖中会阻止死代码消除为纯函数表达式添加/* __PURE__ */注释标记让压缩器terser/esbuild 等确认调用无副作用后可安全移除。配套地package.json 中声明了sideEffects: false同样向打包器承诺模块导入本身无副作用从而允许更大胆的 tree-shaking。对使用者而言这意味着最终 bundle 中未被引用到的 Radix 部件可以被可靠移除。1.1.9通过 CI 重新发布以附带 provenance 认证Republish through CI to attach provenance attestations. The previous versions of these packages were published manually outside of CI and therefore shipped without provenance; this patch re-releases the same code through the CI pipeline so every package includes an attestation.这一条属于供应链安全supply chain security变更此前的版本是在 CI 之外手工发布因而缺少 npm 的provenance来源认证证明——即该包确实由特定 CI 流水线从特定仓库构建发布的可验证声明。1.1.9 在 CI 流水线中重新发布了相同的代码使每个包都附带认证。代码行为没有任何变化纯粹是发布流程与可追溯性层面的改进。顺带将依赖radix-ui/react-use-layout-effect更新到1.1.3。1.1.10回退破坏性变更恢复 React Server Components 兼容性Reverted breaking changes that caused compatibility issues with React Server Components. Updated dependencies:radix-ui/react-use-layout-effect1.1.4这是最新的一个版本当前 package.json 版本即为1.1.10动作是回退了之前引入的、会破坏 React Server ComponentsRSC兼容性的破坏性变更同时把依赖升级到radix-ui/react-use-layout-effect1.1.4。RSC 兼容性的技术支撑点在本包的入口 index.ts 的use client指令以及 SSR 安全的布局效应。Presence 依赖的useLayoutEffect来自 use-layout-effect.tsx在服务端无document时替换为 noop从而规避 React 在服务端调用useLayoutEffect时的告警。回退破坏性变更意味着 1.1.10 重新校准了SSR/RSC 兼容与客户端动画判定之间的平衡回归到此前稳定的行为基线。仓库的apps/ssr-testing应用包含rsc、slot、portal等示例页正是此类兼容性验证的实验场。依赖关系为什么这个包如此牵一发而动全身从 package.json 可以看到其依赖与兼容边界运行时依赖仅一个radix-ui/react-use-layout-effectworkspace:*monorepo 内联依赖peerDependenciesreact与react-dom均支持^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc——从 16.8Hooks 引入一路覆盖到 React 19这也解释了为何 1.1.6 的 React 19 ref 稳定性修复、1.1.10 的 RSC 兼容回退如此重要构建配置source/main/module均指向./src/index.ts源码即入口发布时经radix-build构建为dist产物同时提供 ESM 与 CJS 双格式。上游组件Dialog、Menu、Popover、Toast、Collapsible、Checkbox 等均以workspace:*引用本包因此任何一次 Presence 的发布都会波及整套 Radix 组件在性能、动画与 React 版本兼容性上的表现。这也是 CHANGELOG 中多次出现同步更新依赖版本的原因。从测试看行为契约一个可复用的验收清单presence.test.tsx 用 vitest testing-library 将 Presence 的行为契约固化下来可作为理解乃至在自己实现中复刻其语义的权威参照ref 稳定性ref stability稳定 ref 与不稳定回调 ref内联 attach 时触发重渲染两种场景下渲染次数均需小于 25 且不抛更新深度超限异常子元素 ref 必须收到真实 DOM 节点。挂载/卸载行为mount/unmount behaviorpresent为true时渲染子元素present变false且无退出动画时立即移除false → true重新切换时能恢复挂载。渲染函数子元素render function children以函数作为children时始终强制挂载force-mount元素不离开 DOM仅通过present参数通知调用方切换内容如visible/hidden文案present为true时传入的present参数确实为true。其中函数子元素 强制挂载的语义与 presence.tsx 中const forceMount typeof children function的实现一一对应——当你需要元素始终在 DOM 中、仅切换样式/内容时如某些保持可聚焦性或测量尺寸的场景应优先考虑这种用法。版本变更速查表版本类型核心内容关键源码落点1.1.4修复Presence 内存泄漏事件监听与定时器清理逻辑1.1.5修复正确处理 keyframe 名含转义字符的animationendCSS.escape(event.animationName)比较1.1.6修复React 19 下不稳定 ref 引发的无限循环useStableComposedRefs稳定回调身份1.1.7性能减少强制 reflow布局阶段缓存 animation-namepassive effect 不再实时读取1.1.8构建改善 tree-shaking/* __PURE__ */ 具名渲染函数 sideEffects: false1.1.9发布流程CI 重发布以附带 provenance 认证发布流水线代码无变化1.1.10兼容性回退破坏性变更恢复 RSC 兼容入口use client与 SSR 安全 useLayoutEffect小结从版本历史中可沉淀的工程经验纵观 1.1.41.1.10 的演进可以提炼出三条可复用的工程方法论动画存在性判定避免依赖事件时机由于animationstart在animation-delay后才触发、且缺少animationrun事件Radix 采用对比 computed style 中 animation-name 是否变化的判定方式遇到此类事件迟到问题时优先考虑以样式快照变化作为判定信号。性能优化的核心是缓存干净样式强制 reflow 的代价远高于一次缓存读取把 getComputedStyle 的时机前移到 commit 阶段/布局阶段并缓存复用是低风险高收益的通用优化手段。框架升级期ref 身份稳定性是关键契约React 19 对回调 ref 的分离/附加更激进任何在 ref 中触发状态更新的组件都必须保证 ref 回调身份稳定否则极易踩中无限循环这一点对所有自定义 Hook 组件都有普适参考价值。若希望进一步研究可以直接阅读本仓库的核心实现 presence.tsx、状态机工具 use-state-machine.tsx、回归测试 presence.test.tsx以及实际消费方示例 dialog.tsx形成原理 → 验证 → 应用的完整链路。赞分享前端UI组件【免费下载链接】primitivesRadix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by workos.项目地址https://gitcode.com/gh_mirrors/pr/primitives点击查看免费下载相关推荐Radix Primitives 进度条组件演进实录radix-ui/react-progress 1.1.16 变更历史与源码级解析Radix Primitives 进度条组件演进实录radix ui/react progress 1.1.16 变更历史与源码级解析 radix ui/前端UI组件radix-ui/react-compose-refs 深入解析Radix Primitives 中 ref 组合工具的实现原理与版本演进radix ui/react compose refs 深入解析Radix Primitives 中 ref 组合工具的实现原理与版本演进 导读 radi前端UI组件Radix Primitives react-context-menu 2.3.x 演进全解析版本变更、受控状态与源码实现深度剖析Radix Primitives react context menu 2.3.x 演进全解析版本变更、受控状态与源码实现深度剖析 本文以 radix ui前端UI组件上一篇Guzzle 异常处理指南PSR-18 合规下的异常选择决策树与最佳实践下一篇gix-config纯 Rust 高性能 git-config 文件的读写库深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考