1. 使用场景
  2. 安装
  3. 概念
  4. 创建状态机
  5. 状态节点 state
    1. 子状态(复合状态)
    2. 多状态共存(并行状态)
    3. 临时状态节点 Transient State Nodes
  6. 储存状态(持久化)
  7. 储存变量(扩展状态)
  8. 进入/离开状态时调用函数
  9. 支持 Promise
  10. action 和 invoke 的区别
  11. 活动 activity
  12. 守卫 guarded
  13. 组件化
    1. 演员模型 Actor model
  14. assign 的执行顺序问题
  15. 参考

JavaScript 状态机 xstate 使用简介

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 和 activity
    • invoke
  • 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
idle: {
// 转换
on: {
// 简写, 相当于 {target: 'a'}
'TO_A': 'a',
}
},
// 状态节点 a
a: {
on: {
'TO_B': { target: 'b' }
}
},
b: {}
}
});

// 创建状态机
const service = interpret(machine)
// 输出 idle a b
service.subscribe((state) => console.log(state.value))
service.start()
// 通过发送 事件 来切换状态. 这里的'TO_A'是简写, 相当于 {taype: 'TO_A'}
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')
// 输出
// { idle: 'b' }
// { idle: 'c' }
// 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()
// 输出
// entry idle
// entry a
// { idle: {}, a: {} }

存在多个子状态的并行状态

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()
// 输出
// { idle: { a: {}, b: {} } }

临时状态节点 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()
// 输出
// entry idle
// exit idle
// b

临时状态节点不会执行 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()
// 输出
// entry idle
// exit idle
// b

储存状态(持久化)

状态 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')
// 获取当前状态, 并转成 JSON
const jsonState = JSON.stringify(service.state);
service.stop();
console.log('----------')
// 从 JSON 中恢复状态
const previousState = State.create(JSON.parse(jsonState));
const service2 = interpret(machine)
service2.subscribe((state) => {
console.log(state.value) // a
console.log(state.context) // {foo: 2}
})
// 从指定状态处启动
service2.start(previousState)
// 输出
// idle
// a
// ----------
// a
// { foo: 2 }

注意上文代码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
assign((context, event) => {
console.log(context) // { foo: 1, bar: 1 }
return {foo: 2}
}),
(context) => console.log(context) // { foo: 2, bar: 1 }
]
}
},
});

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()
// 输出
// 2 3 1

另一种官方推荐的写法

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 一个对象,将函数作为需要更新的属性值.
assign({
foo: () => 2
}),
(context) => console.log(context) // { foo: 2 }
]
}
},
});

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')
// 输出
// entry idle
// state: idle
// state: entry exit
// 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')
// 输出
// entry idle
// state: idle
// entry exit
// action a
// state: 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)
// reject(2)
})
},
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()
// 输出
// entry idle
// state: idle
// invoke onDone 1
// state: idle

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')
// 输出
// start activity
// idle
// stop activity
// 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')
// 输出
// entry idle(assign)
// start activity
// run invoke
// entry idle
// idle
// exit idle
// stop activity
// 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'
},
// 报错
// Warning: No implementation found for activity 'activities'
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
// foo: 0
},
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: {
// 接收 actor 发送的事件
'CHILD_END': {
actions: [
(context) => {
console.log('receive child event')
// 停止 actor
context.childRef.stop()
}
]
}
},
entry: [
assign({
// 创建 actor,并赋值给 context.childRef
// 第二个参数表示 childMachine 的唯一名称, 可选
childRef: () => spawn(childMachine, 'childrenName')
}),
// 向 childRef 发送事件
send(
{ type: 'CHILD_TO_A' },
{
to: (context) => context.childRef
// or
// 如果调用 spawn 时传递了第二个参数 name, 也可以这样作
// to: 'childrenName'
}
)
],
}
},
});

const service = interpret(machine)
service.start()
// 输出
// entry child idle
// entry child a
// receive child event

注意只能在 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 文档
前端状态管理设计——优雅与妥协的艺术_唐霜的博客