xstate 是基于 Js 的有限状态机, 支持可视化.
使用场景
xstate 适用于复杂的状态管理, 例如需要在大量状态间切换:
1 RTC 视频通话
2 编辑器 UI 界面管理
安装
1
| npm install xstate@4.31.0 --save
|
概念
- state(状态): 某事/物在某个时间点的概括, 可以随某些条件切换到另一种状态.例如水可以分成三种状态: 固 液 气.写在 stats 属性中
- state node(状态节点): 某个状态的具体配置,写在 states 属性中
- context(上下文): 状态机的扩展状态.其实就是个对象,可以存取变量.写在 context 属性中
- event(事件): 用于描述事件的类型及参数,格式为一个拥有 type 属性的对象
- transition(转换): 如何响应某个事件.写在 state node 的 on 属性中
- effect(副作用): 特定状态/事件的回调函数,可分为两类
- action(动作):
- entry action 进入状态时调用.写在 entry 属性中
- exit action 离开状态时调用.写在 exit 属性中
- transition action 收到某个对应的事件时调用.写在 transition 的 actions 属性中
- activity(活动): 类似于React useEffect.写在 activities 属性中
- invoke(调用): 进入状态时调用,用于支持 Promise 和 Rxjs.写在state node 的 invoke 属性中
- actor(演员): actor model, 用于创建子 actor 及 actor 间通信
- guarded(守卫): 用于判断是否执行对应的 transition.写在 transition 的 cond 属性中
- module(模型)
创建状态机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { createMachine, interpret } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { on: { 'TO_A': 'a', } }, a: { on: { 'TO_B': { target: 'b' } } }, b: {} } });
const service = interpret(machine)
service.subscribe((state) => console.log(state.value)) service.start()
service.send('TO_A') service.send({ type: 'TO_B' })
|
状态节点 state
子状态(复合状态)
分层状态节点 Hierarchical State Node | XState 文档
state 支持设置子状态, 如下所示 idle 状态由 b 和 c 两个子状态组成.这种由多个子状态的状态叫复合状态.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { createMachine, interpret, assign, State } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { initial: 'b', states: { 'b': { on: { 'TO_C': 'c' } }, 'c': {} }, on: { 'TO_A': 'a' }, }, 'a': {} }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start() service.send('TO_C') service.send('TO_A')
|
多状态共存(并行状态)
并行状态节点 Parallel State Node | XState 文档
同一时间可以处于多种状态,这种状态称为 并行状态
可以通过 type: 'parallel' 来开启并行状态,该配置表示进入某个 state, 同时运行所有的子状态.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ type: 'parallel', states: { idle: { entry: () => console.log('entry idle') }, 'a': { entry: () => console.log('entry a') } }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start()
|
存在多个子状态的并行状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { createMachine, interpret, assign, State } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { type: 'parallel', states: { 'a': {}, 'b': {} } }, }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start()
|
临时状态节点 Transient State Nodes
状态节点 | XState 文档
拥有 always 属性的节点叫临时状态节点.
xstate 不会在这个状态停留, 而是立刻切换到 always 中第一个 cond 为 true 的节点.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { 'idle': { entry: () => console.log('entry idle'), exit: () => console.log('exit idle'), always: [ {target: 'a', cond: () => 0}, {target: 'b'}, ] }, 'a': {}, 'b': {} }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start()
|
临时状态节点不会执行 invoke 和 activities, 除非 always 中没有复合条件的配置时,状态机才会停留在临时状态节点.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { 'idle': { entry: () => console.log('entry idle'), exit: () => console.log('exit idle'), invoke: { src: () => { console.log('run action') new Promise((resolve) => resolve()) }, onDone: { actions: () => console.log('run action onDone') } }, activities: ['idleActivity'], always: [ {target: 'a', cond: () => 0}, {target: 'b', cond: () => 1}, ], }, 'a': {}, 'b': {} } },{ activities: { idleActivity: () => { console.log('run activity') return () => { console.log('stop activity') } } } });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start()
|
储存状态(持久化)
状态 State | XState 文档
xState 支持将当前状态储存为 JSON 字符串, 然后通过 State.create 恢复并继续执行.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import { createMachine, interpret, assign, State } from 'xstate';
const machine = createMachine({ initial: 'idle', context: { foo: 1 }, states: { idle: { on: { 'TO_A': 'a' }, exit: assign({ foo: () => 2 }) }, 'a': { } }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start() service.send('TO_A')
const jsonState = JSON.stringify(service.state); service.stop(); console.log('----------')
const previousState = State.create(JSON.parse(jsonState)); const service2 = interpret(machine) service2.subscribe((state) => { console.log(state.value) console.log(state.context) })
service2.start(previousState)
|
注意上文代码34行的 state.context 输出了 { foo: 2 }, 表明 context 的改动也被保存了.
储存变量(扩展状态)
上下文 Context | XState 文档
xstate 中的变量应储存在 context 属性中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: { foo: 1, bar: 1 }, states: { idle: { entry: [ assign((context, event) => { console.log(context) return {foo: 2} }), (context) => console.log(context) ] } }, });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start()
|
一定要注意 assign 包裹的函数会先依次运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: {}, states: { idle: { entry: [ () => console.log(1), assign(() => console.log(2)), assign(() => console.log(3)), ] } }, });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start()
|
另一种官方推荐的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: { foo: 1 }, states: { idle: { entry: [ assign({ foo: () => 2 }), (context) => console.log(context) ] } }, });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start()
|
进入/离开状态时调用函数
动作 Actions | XState 文档
entry 和 exit 会在进入/离开状态时调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { createMachine, interpret } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { on: { 'TO_A': 'a', }, entry: () => console.log('entry idle'), exit: () => console.log('entry exit') }, a: {}, } });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start() service.send('TO_A')
|
如果希望能更精确的控制离开时调用的函数, 可以用 transition (on)的 actions 属性代替.
调用顺序为先执行 exit, 后执行 transition 中的 actions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { createMachine, interpret } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { on: { 'TO_A': { target: 'a', actions: (e) => { console.log('action a') } } }, entry: () => console.log('entry idle'), exit: () => console.log('entry exit') }, a: {}, } });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start() service.send('TO_A')
|
支持 Promise
action 不支持追踪 Promise 的状态,在函数执行完成后就结束了.可以使用 invoke 等待函数返回的 Promise 执行完毕.
invoke.src 绑定的函数在 entry 函数执行后调用. 如果返回 Promise, xstate 会在 Promise 执行完毕后调用 invoke.onDone 或 invoke.onError.
invoke.onDone 和 invoke.onError 属性接受 transition 作为参数, 分别对应 Promise resolve 和 reject.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import { createMachine, interpret } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { invoke: { src: async (context, event) => { return new Promise((resolve, reject) => { resolve(1) }) }, onDone: { actions: (context, event) => console.log('invoke onDone', event.data) }, onError: { actions: (context, event) => console.log('invoke onError', event.data) } }, entry: () => console.log('entry idle'), exit: () => console.log('entry exit'), } }, });
const service = interpret(machine) service.subscribe((state) => console.log('state: ' + state.value)) service.start()
|
action 和 invoke 的区别
类似于 vuex 的 mutations 和 actions.
action 不支持处理 promise, invoke 可以通过设置 onDone 和 onError 属性来获取 promise 的结果.
活动 activity
类似 React 的 useEffect.
activity 写在 states 的 activities 选项中, 在进入对应状态时调用.需要返回一个函数,这个函数在退出状态时被调用.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', states: { idle: { activities: ['idleACtivity'], on: { 'TO_A': 'a' } }, a: {} }, }, { activities: { idleACtivity: () => { console.log('start activity') return () => { console.log('stop activity') } } } });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start() service.send('TO_A')
|
进入状态时的调用顺序为 assign > activity > invoke, 退出顺序为 exit > activity(返回的函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: {}, states: { idle: { activities: ['idleACtivity'], on: { 'TO_A': 'a' }, entry: [ () => console.log('entry idle'), assign(() => console.log('entry idle(assign)')), ], exit: () => console.log('exit idle'), invoke: { src: () => console.log('run invoke') } }, a: {} },
}, { activities: { idleACtivity: () => { console.log('start activity') return () => { console.log('stop activity') } } } });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start() service.send('TO_A')
|
注意 xstate 不支持直接把函数写在 activities 里, 会报错.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: {}, states: { idle: { on: { 'TO_A': 'a' }, activities: () => { console.log('start activity') return () => { console.log('stop activity') } }, }, a: { } }, });
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start()
|
守卫 guarded
守卫(Guarded)转换 | XState 文档
触发某个事件时, 可以通过设置 cond 属性, 来判断是否执行对应的 transition.
cond 属性接受一个函数作为参数, 如果函数返回 true, 则执行对应的 transition.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { createMachine, interpret, assign } from 'xstate';
const machine = createMachine({ initial: 'idle', context: { foo: 1 }, states: { idle: { on: { 'NEXT': [ {target: 'a', cond: 'vaildFoo'}, {target: 'b', cond: (context, event) => !context.foo}, ] }, }, a: {}, b:{} }, },{ guards: { vaildFoo: (context, event) => context.foo > 0 } } );
const service = interpret(machine) service.subscribe((state) => console.log(state.value)) service.start() service.send('NEXT')
|
组件化
演员 Actors | XState 文档
xstate 使用 actor modle 实现了模型间的相互创建与通信
下面是一个在 machine 中创建子 actor,并互相发送事件的例子.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| import { createMachine, interpret, assign, spawn, send, sendParent } from 'xstate';
const childMachine = createMachine({ initial: 'idle', states: { idle: { on: { 'CHILD_TO_A': 'a' }, entry: () => console.log('entry child idle') }, a: { entry: [ () => console.log('entry child a'), sendParent({ type: 'CHILD_END' }) ] } } })
const machine = createMachine({ initial: 'idle', context: { childRef: null }, states: { idle: { on: { 'CHILD_END': { actions: [ (context) => { console.log('receive child event') context.childRef.stop() } ] } }, entry: [ assign({ childRef: () => spawn(childMachine, 'childrenName') }), send( { type: 'CHILD_TO_A' }, { to: (context) => context.childRef } ) ], } }, });
const service = interpret(machine) service.start()
|
注意只能在 assign 中创建子 actor, 且必须用函数返回 spawn (() => spawn())
具体见官方文档注意部分: 演员 Actors | XState 文档
1 2 3
| assign({ childRef: () => spawn(childMachine, 'childrenName') }),
|
演员模型 Actor model
actor modle 是一种并发编程模型/解决方案,其特点有:
- 并发的基本单位称为 actor
- actor 间不相互共享状态
- actor 间通过发送消息通信
- 所有 actor 可以同时运行
assign 的执行顺序问题
xstate 4.x 版本会优先执行 assign, 这个问题会在 5.x 版本修复为依序执行
动作 Actions | XState 文档
参考
XState 状态管理
Getting Started | XState Docs
XState 文档
前端状态管理设计——优雅与妥协的艺术_唐霜的博客