Zag 状态机引擎 @zag-js/core 深度解析:从核心模型到关键修复演进 前端UI组件【免费下载链接】zagBuild your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines项目地址https://gitcode.com/gh_mirrors/za/zag点击查看免费下载zag-js/core是 Zag 设计系统的基础运行库它提供了一套面向 UI 组件的有限状态机FSM最小实现驱动 React、Solid、Vue、Svelte、Preact 与 Vanilla 六种框架的数十个组件Dialog、Menu、Select、Slider 等。本指南以 packages/core/CHANGELOG.md 的版本演进记录为主线结合 packages/core/README.md、packages/core/src 源码与 packages/core/tests 测试用例系统讲解状态机配置模型、嵌套状态解析、mergeProps合并语义、事件替换机制、watch 比较器与守卫体系等核心能力帮助你理解 Zag 组件逻辑与视图分离的架构根基并掌握其关键的踩坑点与设计取舍。一、包定位XState FSM 的最小化定制实现根据 packages/core/README.md 的定义zag-js/core是 XState FSM 的最小化实现并额外加入了 Zag 组件所需的特性。它覆盖以下能力有限状态且支持可选嵌套层级状态nested/hierarchical states初始状态initial state转换transitions支持字符串或对象语法上下文context进入动作entry actions、退出动作exit actions、转换动作transition actions副作用effects布尔守卫辅助函数and、or、not值得注意的是该核心包刻意保持精简它不包含延迟转换delayed transitions、间隔调度interval scheduling、spawn 辅助函数或 activitiesREADME 明确建议使用 actions/effects 来执行副作用。从 packages/core/package.json 可以看到它的依赖面极小仅依赖zag-js/utils与zag-js/dom-query两个工作区包sideEffects: false便于打包器做 tree-shaking。快速上手非常简单createMachine定义机器配置再由框架适配器如zag-js/vanilla创建运行时服务import { createMachine } from zag-js/core const toggleMachine createMachine({ id: toggle, initialState() { return inactive }, states: { inactive: { on: { TOGGLE: active } }, active: { on: { TOGGLE: inactive } }, }, })服务层负责订阅与发送事件import { createMachine } from zag-js/core import { VanillaMachine } from zag-js/vanilla const machine createMachine({ initialState() { return inactive }, states: { inactive: { on: { TOGGLE: active } }, active: { on: { TOGGLE: inactive } }, }, }) const service new VanillaMachine(machine) service.start() service.subscribe(() { console.log(service.state.get()) }) service.send({ type: TOGGLE }) service.send({ type: TOGGLE }) service.stop()从 packages/core/src/create-machine.ts 的源码可以看到createMachine本身相当轻——它只负责调用ensureStateIndex(config)构建状态索引用于后续状态解析与校验然后原样返回配置对象真正的运行时行为由各框架适配器消费该定义后驱动。二、机器配置 Schemacontext、computed 与 implementationscreateMachine(config, options)的config遵循如下 Schema详见 packages/core/README.md 的 API 章节类型定义见 packages/core/src/types.ts配置项类型说明initialState函数返回机器的初始状态值states对象状态名到状态配置的映射支持嵌套states与initial子状态on对象可选的全局转换任意状态下可用context函数返回可绑定bindable的上下文字段computed对象基于 context、props、refs 的派生值entry/exit/effects函数/数组根级动作与副作用implementations对象actions、guards、effects的查找表状态配置State config核心是on对象将事件类型映射到转换配置。转换配置Transition config支持两种语法字符串语法(string)即目标状态名等价于{ target: stateName }对象语法target(string) —— 要转换到的状态名actions(Action | Action[]) —— 该转换被触发时执行的动作guard(Guard) —— 条件谓词函数返回true才执行转换动作函数接收两个参数context机器当前上下文与event触发该动作的事件对象。类型层面packages/core/src/types.ts 中的Transition还支持reenter?: boolean标志用于显式状态重入——这与 CHANGELOG 0.1.6 中添加显式状态重入支持以摆脱after: 0hack的演进记录相呼应。机器选项Machine optionsactions、guards、activities、delays四个查找对象用于将字符串形式的动作/守卫按名称解析为具体实现。此外 0.2.4 引入的compareFns选项用于为watch键提供自定义相等性判断见第七节。三、嵌套状态与转换目标解析dot notation 与#idCHANGELOG 1.35.0PR #2985为机器加入了全面的嵌套状态支持与测试覆盖在 README 中补充了层级状态的说明并为嵌套转换确保 effects/entry/exit 的正确顺序还在各框架适配器中加入了深度嵌套的冒烟测试。3.1 dot notation 嵌套状态子状态通过点号路径表示父状态需用initial声明默认子状态import { createMachine } from zag-js/core const machine createMachine({ initialState() { return dialog }, states: { dialog: { tags: [overlay], initial: closed, states: { closed: { on: { OPEN: { target: dialog.open } } }, open: { on: { CLOSE: { target: dialog.closed } } }, }, // 父级转换仍然生效 on: { RESET: { target: dialog.closed } }, }, }, }) // service.state.matches(dialog.open) true 当嵌套状态激活时当嵌套状态处于激活态时service.state.matches(dialog.open)返回true——matchesState的语义在 packages/core/src/state.ts 中定义为完全相等或以state .开头即祖先状态视为匹配。3.2 XState 风格#id目标1.35.1 为转换目标增加了 XState 风格的#id支持#...目标按状态节点id解析使跨层级转换变得显式。其规则是状态 ID 在同一台机器内必须唯一import { createMachine } from zag-js/core const machine createMachine({ initialState() { return dialog }, states: { dialog: { initial: open, states: { focused: { id: dialogFocused, }, open: { initial: idle, states: { idle: { on: { CLOSE: { target: #dialogFocused }, }, }, }, }, }, }, }, })3.3 源码级解析算法从 packages/core/src/state.ts 的源码看机器创建时通过buildStateIndex递归遍历状态树构建两条索引index路径 → 状态定义与idIndexstate id → 路径并缓存于WeakMapstateIndexCache中避免重复构建。resolveStateValue(machine, value, source)state.ts是转换目标解析的核心其优先级依次为显式绝对路径#id前缀→ 通过idIndex反查路径未知 ID 抛出[zag-js] Unknown state id: ...点前缀子目标如.idle源为open→open.idle→ 在源状态下追加子路径裸名称 兄弟解析与 XState/SCXML 语义对齐→ 从父作用域向上逐层查找永不解析为源的子节点最后按绝对路径解析并通过initial链下钻到最深层初始子状态resolveAbsoluteStateValuestate.ts。这一算法被 packages/core/tests/state.test.ts 中的用例严格验证几个关键边界值得记住当兄弟与子节点同名时优先解析为兄弟——例如 color picker 的open状态同时存在子节点idle与根级兄弟idletarget: idle必须落在根级兄弟上state.test.ts#L40-L68裸名称永不解析为源的子节点想要显式指向子节点必须使用点前缀.dstate.test.ts#L126-L155目标先解析到祖先再经initial下钻如.open从dialog出发会解析为dialog.open.viewingstate.test.ts#L100-L124未知目标返回原始字符串而非抛错便于处理无匹配情况而未知#id则直接抛错state.test.ts#L304-L328。3.4 转换查找与退出/进入集合findTransitionstate.ts从状态链的最深层子状态开始向上冒泡查找事件处理先查叶子状态自身的on再逐级查祖先on最后查根级machine.on——测试用例验证了这一就近优先、逐级冒泡的语义state.test.ts#L388-L400。getExitEnterStatesstate.ts计算新旧状态链的公共前缀得出最小化的 exiting/entering 状态集合当reentertrue且新旧叶子相同时会重建整条链以触发完整重入。1.43.1 的修复正是针对该函数在不支持Array.prototype.at()的浏览器如 Safari 15.4 以下中会抛错修复后不再依赖该 API。四、机器创建期的状态校验把错误拦在运行时之前1.38.1 为createMachine增加了复合状态校验状态若含有子状态但未声明initial或initial引用了不存在的子状态都会在机器创建时直接抛错。实现位于buildStateIndex中state.ts[zag-js] Compound state open has child states but no initial property [zag-js] Compound state open has initial nonexistent which is not a child state同时重复的状态 ID 也会抛错[zag-js] Duplicate state id: ...state.ts。对应的测试在 packages/core/tests/state.test.ts覆盖了三种非法配置。这套前置校验把配置错误从难以排查的运行时行为提前到了机器创建时刻是保证几十个组件机器定义健壮性的重要防线。五、mergeProps跨框架 Props 合并的演进史mergeProps是 Zag 各框架适配层共用的核心工具负责把机器计算出的 props 与用户传入的 props 智能合并。CHANGELOG 中关于它的修复贯穿多个版本几乎可以单独构成一部演进史版本修复内容0.7.0重构mergeProps的正则表达式0.27.0修复className为undefined时抛错的问题0.51.2修复事件on*合并逻辑1.23.0修复合并后符号键symbols被剥离的问题1.24.1修复props为undefined/null时抛错的问题1.43.0组合data-ownedby值、DOM 查询按 token 匹配 owner并暴露isOwnedBy做归属检查1.45.0修复含分号的 inline style 值被截断的问题覆盖引号包裹的 CSS 自定义属性与 data URLs5.1 合并语义从 packages/core/src/merge-props.ts 的源码看合并遵循以下语义事件合并on*且双方都是函数时用callAll将两个处理器依次调用class 合并className/class用clsx拼接并去空style 合并字符串与对象互相转换后做属性级合并data-ownedby合并token 去重后按空格拼接ownedBy辅助函数普通属性覆盖后传入的非undefined值覆盖先前值符号键保留通过Object.getOwnPropertySymbols显式拷贝符号键Svelte 的附件机制依赖此行为。5.2 1.45.0 的样式解析器1.45.0 的修复最具代表性旧实现用简单正则按分号切分 style 字符串导致引号内的分号如--label: a;b或data URL 内的分号如url(data:image/svgxml;base64,...)被错误截断。新实现serializemerge-props.ts是一个逐字符扫描的状态机维护depth括号深度、quote引号状态、comment注释状态三个状态引号内的分号不结束声明支持转义\url(...)、image-set(...)等嵌套函数内的分号不结束声明/* ... */注释内的分号被忽略仅当depth 0且不在引号/注释中时才按;切分声明再交由CSS_REGEX提取属性名与值。packages/core/tests/merge-props.test.ts 用 7 个用例覆盖了该解析器普通样式拆分、引号内分号保留、转义引号、data URL 与嵌套函数、注释内分隔符等merge-props.test.ts#L22-L70。同时该测试也验证了data-ownedby的 token 合并与去重merge-props.test.ts#L4-L20并确认data-state等其他 data 属性仍保持后者覆盖前者的普通覆盖行为。六、同一 tick 内的最后一次调用生效setOpen 修复与事件替换机制1.44.0 修复了一个隐蔽的时序 bugapi.setOpen忽略同一 tick同一次同步执行内的第二次调用——setOpen(true)立即紧跟setOpen(false)时组件保持打开而非关闭只有两次调用间await一个微任务才生效。修复后的规则是同一 tick 内最后一次调用生效last call wins用相同值重复调用setOpen仍然只触发一次onOpenChangeDrawer 的api.setSnapPoint存在相同问题修复后行为一致。这一语义的底层支撑是 packages/core/src/replace-tracker.ts 中的createReplaceTracker它按 key 记录最新的排队事件每个 claim 分配一个唯一 symbol token若某事件在真正执行前已被同 key 的更新事件替换isReplaced返回true则该事件被丢弃。其行为被 packages/core/tests/replace-tracker.test.ts 完整覆盖单次 claim 不被替换、后 claim 替换先 claim、多次 claim 仅最新者存活、不同 key 相互独立replace-tracker.test.ts#L4-L42。事件类型层面packages/core/src/types.ts 的EventType也为此预留了replaces?: string字段——当设置时相同 key 的后续事件会替换尚未执行的旧事件这正是 replace tracker 在类型层的映射。七、watch 与自定义相等性比较CHANGELOG 0.2.4 为watch属性键引入了自定义相等性函数compareFns选项用于比较复杂对象或日期等值避免不必要的上下文更新。文档给出了完整示例const machine createMachine( { context: { date: new Date(), }, watch: { date: [invokeOnDateChange], }, }, { compareFns: { date: (a, b) a.getTime() b.getTime(), }, }, )围绕 watch 的演进还有几条关键记录0.2.5新增通过*键监视全部 context 属性的能力0.2.11出于某些原因回退了对*的监视支持PR #565说明通配监听在实现上存在取舍0.35.0PR #1203通过只订阅一次 context显著提升 watcher 的运行时性能——将原本可能多次订阅同一 context 的路径收敛为单次订阅。这与 packages/core/src/memo.ts 中基于isEqual的依赖记忆化工具配合memo对比依赖数组长度与每个依赖的相等性仅在变化时重算结果并触发onChange为 context/computed 的按需求值提供基础设施。八、守卫体系and / or / not 组合与 isIn 断言核心包提供布尔守卫辅助函数。createGuardscreate-machine.ts导出的and/or/not分别对应every、some与取反语义可组合多个守卫import { createGuards } from zag-js/core const { and, or, not } createGuards() // and(g1, g2) → 所有守卫通过 // or(g1, g2) → 任一守卫通过 // not(g1) → 守卫不通过0.1.5 进一步扩展了守卫表达式支持in guards——即断言机器处于给定状态的守卫可内联使用或放入 guards 选项参考 statecharts 术语中的 in/not-in guardsimport { guards } from zag-js/core const { isIn } guards const machine createMachine({ on: { CLICK: { guard: isIn(open), }, }, states: { open: {}, closed: {}, }, })另外0.5.0 拓宽了watch的类型以支持choose辅助函数并新增initialContext与initialState到meta选项setupcreate-machine.ts则同时暴露guards、createMachine与choose基于params.choose做条件化转换选择三件套构成类型安全的状态机定义入口。九、Scope跨 Shadow DOM 的 DOM 查询抽象Zag 组件需要同时工作在普通 DOM 与 Shadow DOM 环境中为此核心包提供了Scope抽象。0.1.4 首次为 machines 加入 shadow root 的增量支持createScopepackages/core/src/scope.ts基于可选的getRootNode构建完整的查询工具集getRootNode()—— 返回ShadowRoot | Document默认documentgetDoc()/getWin()—— 解析根节点所属的 document 与 windowgetActiveElement()/isActiveElement()—— 活动元素查询与判断getById(id)—— 在根节点范围内按 ID 查询元素类型为Element。1.41.0 将Scope.getRootNode()的返回类型从Node收窄为Element从而让调用方可以直接使用querySelector/querySelectorAll——这修正了类型层面Node 没有这些方法的缺陷属于典型的 API 类型演进。对应的类型定义见 packages/core/src/types.ts。十、性能与运行时演进从 1.0.0 重写说起CHANGELOG 中有一条重要的性能里程碑1.0.0PR #2233重写全部 machines 以提升性能与初始挂载速度标注各组件获得约1.5x – 4x的性能提升。同版本还包含两个 Breaking ChangeToast 必须用createStore创建 toast storeSolid.js 中必须使用Key组件渲染 toasts。围绕运行时的关键演进还包括0.33.0PR #1144只创建一次 machine 实例用setContext(...)替代withContext(...)更新上下文减少重复实例化开销0.35.0watcher 只订阅一次 context见第七节0.19.1内部支持按需停止 activity0.13.0支持在写入机器 context 之前转换 context 值便于将某些值传给ref函数0.2.2提供嵌套 context 值的深度合并工具deep merge并修复computed的类型0.1.2 / 0.1.1修复 computed 属性在 fast-refresh 下被删除、以及与hookSync选项配合失效的问题0.2.1改进内部 machine options 的更新支持并修复 React 受控 context。在打包与兼容性层面0.11.0 改用 tsup 重建全部包、0.10.3 改用 vite 构建、0.1.11 切换到 ESM 与type: module、0.2.0 将编译目标定为es2019以兼容不支持es2020的环境如 Cypress、0.2.7 又切到es2020以支持import.meta.env、0.71.0 起不再在发布的包中携带src文件、0.78.3 移除了 store 与 snapshot 对 promises 的支持建议改用框架专用 hooks 管理异步。0.62.1 还修复了 ReactuseMachine在 HMR 场景下可能触发的Maximum call stack exceeded回归。十一、框架适配 API 的统一演进connect 与 prop getters早期 Zag 以 React 为基准导致跨框架类型管理复杂。0.1.7是标志性的 Breaking Change移除PropTypes导出改为在api.connect中传入normalizePropsReact 也必须使用该模式Vue/Solid 从api.connectPropTypes(state, send, normalizeProps)简化为api.connect(state, send, normalizeProps)类型由normalizeProps自动推断。0.54.0PR #1545再次推进所有 machines 改用prop getters——利用 getter 的惰性求值提升渲染性能同时减少未来 API 变更的破坏面。同版本还记录了 0.20.0 移除 toggle machine942db6ca、0.1.6 新增onChange方法监听 context 变化、支持debug: true可视化状态转换、修复转换对象被突变的问题以及 0.1.3 修复start/stop方法的对称性问题。类型工具方面0.1.5 引入了三个推导辅助StateFrom从任意 machine 推导状态信息、ContextFrom推导上下文信息、EventFrom推导事件类型配合MachineSchematypes.ts中的props/context/refs/computed/state/tag/guard/action/effect/event泛型插槽让整套机器定义在编译期即可获得完整的类型推导。结语从 0.1.0 到 1.45.0 的能力全景回看 packages/core/CHANGELOG.md 的演进主线zag-js/core的成长路径清晰可辨从 0.1.0 的初始发布逐步补齐嵌套状态1.35.0、#id转换目标1.35.1、创建期状态校验1.38.1、事件替换语义1.44.0等状态机能力同步打磨mergeProps的边界行为symbols、undefined、引号分号、data-ownedby并在 1.0.0 完成面向性能的机器重写。对组件库使用者而言这些细节决定了 API 调用的手感如setOpen的 last-call-wins、data-ownedby的归属组合、Shadow DOM 下的 DOM 查询对想要深入理解 Zag 架构的开发者而言packages/core/src/state.ts、packages/core/src/merge-props.ts 与 packages/core/tests 三组文件构成了阅读其状态机实现的最佳入口。赞分享前端UI组件【免费下载链接】zagBuild your design system in React, Solid, Vue, Svelte or Vanilla. Powered by finite state machines项目地址https://gitcode.com/gh_mirrors/za/zag点击查看免费下载相关推荐Zag 1.x 版本演进全解析从状态机架构到 1.45 关键能力Zag 1.x 版本演进全解析从状态机架构到 1.45 关键能力 本文以 Zag 开源仓库根目录 CHANGELOG.md https://link.gitc前端UI组件Zag 状态机调试利器 zag-js/stringify-state从 API 到源码的完整实战指南Zag 状态机调试利器 zag js/stringify state从 API 到源码的完整实战指南 在 Zag 中调试一个组件状态机时你往往需要把 Se前端UI组件draw.io 桌面版 免费离线画图工具draw.io 桌面版 免费离线画图工具 给内网设备画网络拓扑文件上不了云draw.io 桌面版drawio desktop把开源画图编辑器整个装进 E前端UI组件上一篇抖音无水印批量下载器终极指南3分钟掌握高清视频保存技巧下一篇5分钟掌握WorkshopDL突破平台限制的终极模组下载方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考