- 前端
- 后端
【免费下载链接】xstate
State machines, statecharts, and actors for complex logic
XState 是一个用于建模复杂逻辑的 JavaScript/TypeScript 库,其核心理念是状态机(state machines)、状态图(statecharts)与 Actor 模型。本篇文章基于当前仓库xs/xstate中examples/7guis-1-counter-vue这一示例,讲解如何用XState v5 + Vue 3 + TypeScript + Vite构建 7GUIs("7 Graphical User Interfaces" 基准测试套件)中的第一个任务 —— 一个仅含"显示计数 + 递增按钮"的 Counter。读完后你将掌握:用setup()定义类型安全的机器、用assign内建动作更新context、用@xstate/vue的useMachine()在 Vue 组件中接入状态机,以及如何在本地运行和在线预览这个示例。
说明:仓库中该示例的依赖版本为
xstate ^5.28.0、@xstate/vue ^3.1.4、vue ^3.5.29、vite ^5.4.21(见 examples/7guis-1-counter-vue/package.json),下文所有代码均以此为准。
1. 示例概览与 7GUIs 背景
7GUIs 是 Eugen Kiss 提出的一组经典 GUI 基准任务,用于检验不同 UI 框架/工具在典型交互场景下的表达能力,Counter 是其第 1 个任务(原文档在 examples/7guis-1-counter-vue/README.md 中明确了这一点)。任务需求很简单:显示一个数值,点击按钮使其加一。
本示例把它实现为一个状态机驱动的 Vue 应用,文件结构如下(全部位于examples/7guis-1-counter-vue/):
src/counterMachine.ts—— 定义 Counter 状态机(业务逻辑核心)src/Counter.vue—— 使用useMachine接入状态机并渲染 UI 的组件src/App.vue—— 仅负责挂载Counter的根组件src/main.ts—— Vue 应用入口src/style.css—— 7GUIs 风格的全局样式vite.config.ts、package.json、tsconfig.json等工程配置
2. 用setup()定义类型安全的计数器状态机
counterMachine.ts是本例的灵魂,完整代码如下(见 examples/7guis-1-counter-vue/src/counterMachine.ts):
import { setup, assign } from 'xstate'; export const counterMachine = setup({ types: { context: {} as { count: number }, events: {} as { type: 'increase' } } }).createMachine({ context: { count: 0 }, id: 'Counter', initial: 'ready', states: { ready: { on: { increase: { target: 'ready', actions: assign({ count: ({ context }) => context.count + 1 }) } } } } });2.1setup():XState v5 的类型安全入口
XState v5 引入了setup()作为创建机器的新推荐方式。从源码看(packages/core/src/setup.ts),setup接受一个包含schemas、types、actors、actions、guards、delays的配置对象,返回一个SetupReturn对象,其上挂有createMachine、createAction、createStateConfig、extend以及assign、sendTo、raise、log、emit、spawnChild等内建动作(见 packages/core/src/setup.ts)。
在本例中,setup的types字段声明了两类类型信息:
context: {} as { count: number }—— 声明机器上下文中有一个count: number字段;events: {} as { type: 'increase' }—— 声明该机器唯一可接收的事件是{ type: 'increase' }。
之后调用.createMachine({ ... })生成实际机器。这种"先声明类型、再定义配置"的方式,让后续配置中的context、on、actions等全部获得编译期类型检查:比如向send传递{ type: 'decrease' }这类未声明的事件会直接报错,这正是 XState v5 强类型体验的核心。
2.2context:机器的可读状态
context: { count: 0 }定义了机器的初始上下文。在状态机术语中,context是"与状态正交的可变数据",与"当前处于哪个状态(state value)"共同构成快照(snapshot)。本例只关心计数数值,因此把count放进context,而状态本身仅有一个ready。
2.3id与initial/states
id: 'Counter'给机器一个稳定标识,便于调试与引用;initial: 'ready'指定初始状态;states定义状态集合,这里只有一个ready状态,其中on声明了事件到转移的映射。
虽然这个例子只有单个状态、结构极简,但它完整展示了状态机的基本骨架。当业务变复杂时,可以在states中扩展多个状态与转移,这也是 XState 处理复杂逻辑的核心优势。
2.4assign:更新 context 的唯一内建方式
转移中的actions: assign({ count: ({ context }) => context.count + 1 })是理解本例的关键。assign是 XState 的内建动作(built-in action),专门用于不可变地更新机器的 context。
从实现看(packages/core/src/actions/assign.ts),assign接收一个"赋值器"(assignment),可以是:
- 一个函数:
({ context, event }) => partialContext; - 一个属性映射对象:
{ count: ({ context }) => context.count + 1 }(本例所用形式),其中每个字段的值可以是函数(根据当前context/event计算新值),也可以是常量。
在resolveAssign中(packages/core/src/actions/assign.ts),最终通过Object.assign({}, snapshot.context, partialUpdate)生成一份新的 context,再调用cloneMachineSnapshot产出新快照 —— 也就是说,assign不会就地修改旧 context,而是返回不可变的新快照,这与 Vue 的响应式心智模型天然契合。
需要特别注意的是:assign是声明式动作,不能把它直接写进普通自定义动作里命令式调用(开发模式下会给出警告,见 packages/core/src/actions/assign.ts)。
2.5 一次点击背后的完整转移语义
当收到increase事件时,状态机执行一次转移:
- 事件匹配
ready状态的on.increase; target: 'ready'表明转移目标仍是ready状态(即"自转移",self-transition),这是计数器的典型写法 —— 我们不离开当前状态,只更新数据;actions中的assign被解析执行,count从context.count加一;- 状态机发出新的快照,
@xstate/vue的订阅机制将其同步到组件。
3. 在 Vue 组件中接入状态机
3.1Counter.vue:useMachine 的两种关键返回值
examples/7guis-1-counter-vue/src/Counter.vue 完整代码如下:
<script setup lang="ts"> import { useMachine } from '@xstate/vue'; import { counterMachine } from './counterMachine'; const { snapshot, send } = useMachine(counterMachine); </script> <template> <main class="case center-children"> <span>{{ snapshot.context.count }}</span> <button @click="send({ type: 'increase' })">+</button> </main> </template>useMachine来自@xstate/vue。从源码看(packages/xstate-vue/src/useMachine.ts),它本质上是useActor的别名,返回三个值:
snapshot: Ref<SnapshotFrom<TMachine>>—— 一个 Vue 响应式引用,存放机器当前快照;模板中通过snapshot.context.count读取计数;send: (event) => void—— 发送事件的函数,模板中按钮点击时send({ type: 'increase' });actorRef: Actor<TMachine>—— 底层 actor 引用,需要直接操作 actor(如订阅、停止)时才用到。
在useActor内部(packages/xstate-vue/src/useActor.ts),useActorRef负责创建并持有 actor,useSelector(actorRef, (s) => s)用于订阅快照变化并把最新快照写入snapshot这个 Ref —— 当assign产生新快照时,Vue 的响应式系统自动触发视图更新,<span>中的数字随之刷新。此外,useActor在开发模式下会校验传入的是"逻辑"而非"已创建的 ActorRef",传错会直接抛错提示(packages/xstate-vue/src/useActor.ts)。
3.2App.vue与main.ts
App.vue(examples/7guis-1-counter-vue/src/App.vue)只是简单地渲染<Counter />,保持组件层级清晰;main.ts(examples/7guis-1-counter-vue/src/main.ts)是标准 Vue 入口:createApp(App).mount('#app')。
3.3 样式与 7GUIs 观感
examples/7guis-1-counter-vue/src/style.css 实现了 7GUIs 演示常见的"卡片 + 大按钮"视觉:.case是圆角卡片容器,span以大号等宽字体显示数字,button使用浅绿主题并带按下位移效果,hover/focus 也做了处理。这些细节并非功能必需,但让示例在演示时更接近 7GUIs 基准的呈现风格。
4. 运行、构建与在线预览
4.1 本地运行
仓库使用 pnpm 工作区管理,示例自身携带package.json与pnpm-lock.yaml(examples/7guis-1-counter-vue/package.json)。在示例目录内:
# 安装依赖(在仓库根目录或示例目录执行均可,工作区会解析 @xstate/vue 与 xstate) pnpm install # 启动 Vite 开发服务器 pnpm devdev脚本即vite,启动后按终端提示打开本地地址即可看到计数器页面。
4.2 生产构建与预览
package.json中的scripts提供了完整构建链路:
build:vue-tsc && vite build—— 先执行vue-tsc对.vue与.ts做类型检查,再打包到dist/。这也是验证"类型安全"设计的最佳实践:类型不匹配会在构建期暴露;preview:vite preview—— 本地预览构建产物。
4.3 一键在线体验
原文档提供了两个在线预览入口(对应 examples/7guis-1-counter-vue/README.md 的 Live 小节):CodeSandbox 与 StackBlitz 均支持直接打开本示例仓库目录运行,无需本地安装,适合快速体验或分享演示。
5. 从源码看 XState v5 的完整链路
结合上面的讲解,把本例涉及的调用链串起来看:
- 定义机器:
setup({ types: {...} }).createMachine({...})(packages/core/src/setup.ts)内部把配置连同schemas交给createMachine,生成StateMachine实例; - 创建 actor:
useActorRef将机器作为 actor logic 启动,得到一个可接收事件的 actor; - 发送事件:
send({ type: 'increase' })把事件送入机器; - 执行转移:机器在
ready状态命中on.increase,执行assign;resolveAssign计算新 context 并cloneMachineSnapshot出新快照(packages/core/src/actions/assign.ts); - 通知订阅者:actor 发布新快照,
useActor中注册的 listener 更新snapshotRef(packages/xstate-vue/src/useActor.ts); - 视图刷新:Vue 响应式系统驱动模板重新渲染,页面上数字 +1。
这条链路展示了 XState 的一个核心心智模型:UI 只负责把事件交给 actor、把快照渲染出来;状态如何变化、数据如何更新,完全由状态机决定。这也是从 7GUIs Counter 走向更复杂状态图(并行状态、守卫、子状态机、actor 通信等)时的统一基础。
6. 延伸:把 Counter 泛化为你的第一个 XState 应用
本例虽小,但麻雀虽小五脏俱全。基于同一套代码骨架,你可以轻松扩展:
- 增加事件:在
types.events中加入{ type: 'decrease' }、{ type: 'reset' },再在ready.on中补对应转移; - 增加状态:在
states中加入counting、paused等状态,用事件驱动状态切换; - 增加守卫与延迟:通过
setup({ guards, delays })声明,并在转移中使用guard与after; - 复用机器:
setup()返回对象上的extend可以基于已有类型声明扩展 actions/guards/delays(packages/core/src/setup.ts)。
仓库examples/目录还提供了大量同类参考:Vue 版的温度转换器(examples/7guis-2-temperature-vue)、React 版 Counter(examples/7guis-counter-react)、带本地存储的 React 计数器(examples/local-store-counter-react)、计时器(examples/timer)与秒表(examples/stopwatch)等,均采用同样的"机器文件 + 框架 hook"组织方式,是继续学习的极佳素材。
7. 小结
本文围绕examples/7guis-1-counter-vue完整拆解了"用 XState v5 状态机驱动 Vue 计数器"的实践:setup()提供类型安全入口,assign不可变地更新 context,useMachine/useActor把快照与事件桥接到 Vue 响应式系统。掌握这条链路,你就掌握了 XState v5 应用的最小完备单元 —— 无论是 7GUIs 的后续任务,还是真实业务中的复杂交互逻辑,都可以在此基础上逐步叠加状态、守卫、子状态机与 actor 协作来构建。
- 前端
- 后端
【免费下载链接】xstate
State machines, statecharts, and actors for complex logic
相关推荐
XState v5 实现 7GUIs Counter:React + TypeScript + Vite 状态机计数示例完整指南
XState v5 实现 7GUIs Counter:React + TypeScript + Vite 状态机计数示例完整指南 本指南围绕 XState 官方
前端后端XState v5 + React 实战:用类型安全状态机构建 7GUIs Flight Booker 航班预订表单
XState v5 + React 实战:用类型安全状态机构建 7GUIs Flight Booker 航班预订表单 导读 本文以仓库中的 7GUIs Flig
前端后端用 XState v5 状态机实现 7GUIs 温度转换器:React 双向输入的事件驱动实战
用 XState v5 状态机实现 7GUIs 温度转换器:React 双向输入的事件驱动实战 本文以仓库内 examples/7guis temperatur
前端后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考