Context & Actions
Context & Actions
一个 Machine 的状态(state) 是有限的,例如水的状态 (固、液、气、等离子),但我们仍然会需要储存非定性的可变资料(data),这些资料我们会储存在 context 中,如下:
const machine = Machine({
context: {
// 资料 (data) 存在 context 裡,key 可以自己订
count: 0,
user: null,
},
states: {
//...
},
});
我们可以透过 withContext() 动态的给定初始资料,如下:
const myMachine = machine.withContext({
count: 10,
user: {
name: "Jerry",
},
});
在任何状态下,我们都可以拿到 context 的值:
machine.initialState.context;
// { user: null, count: 0 }
const service = interpret(machine.withContext({
count: 10,
user: {
name: 'Jerry'
},
});
service.start();
service.state.context;
// { user: { name: 'Jerry' }, count: 10 }
至于要如何在特定的状态中改变 machine 内的 context 呢?我们会需要用到 Assign Actions。Actions 是一种 理射后不理 (Fire-and-forget)的 Effect,专门用来处理单一次的作用,另外在 XState 中还有许多不同种类的 Effects。
Effects
在 Statecharts 的世界裡,Side Effect 可以依行为区分为两类:
- Fire-and-forget effects - 指执行 Side Effect 后不会另外送任何 event 回 statechart 的 effect。
- Invoked effects - 指除了可执行 Side Effect 之外还能发送和接收 events 的 effect。
这两类 Effect 在 XState 中依据不同的使用方式,又可以分为:
- Fire-and-forget effects
- Actions - 用于单次、离散的 Effect
- Activities - 用于连续的 Effect
- Invoked effects
- Invoked Promises
- Invoked Callbacks
- Invoked Observables
- invoked Machines
Actions
Action 本身就是一个 function,接收三个参数分别是 context, event 以及 actionMeta,context 就是当前 machine 的 context,event 则是触发当前状态切换的事件,actionMeta 则会存放当前的 state 以及 action 物件。
const action = (context, event, actionMeta) => {
// do something...
};
我们可以把 actions 写在任何 State 的任何事件裡,如下:
const lightMachine = Machine({
initial: "red",
states: {
red: {
on: {
CLICK: {
// 转换到 green 的状态
target: "green",
// transition actions
actions: (context, event) => console.log("hello green"),
},
},
},
green: {
on: {
CLICK: {
target: "red",
// transition actions
actions: (context, event) => console.log("hello red"),
},
},
},
},
});
另外还有两种 actions,分别是在进入 state 以及离开 state 时触发,如下:
const lightMachine = Machine({
initial: "red",
states: {
red: {
// entry actions
entry: (context, event) => console.log("entry red"),
// exit actions
exit: (context, event) => console.log("exit red"),
on: {
CLICK: {
target: "green",
},
},
},
//...
},
});
在进入 red 状态时会触发 red 内部的 entry,在离开 red 状态时会触发 red 内部的 exit。这两种 actions 我们称为 entry actions 以及 exit actions。另外 actions 可以定义在 machine options 内,并透过 string 来指定执行的 action,如下:
const lightMachine = Machine({
initial: 'red',
states: {
red: {
// entry actions
entry: 'entryRed'
// exit actions
exit: 'exitRed',
on: {
CLICK: {
target: 'green',
// transition actions
actions: 'redClick',
},
}
},
//...
}
}, {
actions: {
entryRed: (context, event) => console.log('entry red'),
exitRed: (context, event) => console.log('exit red'),
redClick: (context, event) => console.log('hello green'),
},
});
所有设定 actions 的地方都可以是一个 array,依序执行多个 actions,如下:
const lightMachine = Machine(
{
initial: "red",
states: {
red: {
// entry actions
entry: ["entryRed", "temp"],
// exit actions
exit: ["exitRed", "temp"],
on: {
CLICK: {
target: "green",
// transition actions
actions: ["redClick", "temp"],
},
},
},
//...
},
},
{
actions: {
entryRed: (context, event) => console.log("entry red"),
exitRed: (context, event) => console.log("exit red"),
redClick: (context, event) => console.log("hello green"),
temp: (context, event) => console.log("temp"),
},
}
);
在实务开发上,不建议直接把 action function inline 在 machine config 裡,如下,这会造成之后难以除错、测试以及图像化。
CLICK: {
target: 'gerrn',
actions: (context, event) => console.log('hello green')
}
建议统一把 actions 放在 machine options 内,如下:
const lightMachine = Machine(
{
initial: "red",
states: {
red: {
// entry actions
entry: ["entryRed", "temp"],
//...
},
//...
},
},
{
actions: {
entryRed: (context, event) => console.log("entry red"),
temp: (context, event) => console.log("temp"),
},
}
);
Assign Action
assign 是一个 function 专门用来更新 machine context,它吃一个 assigner 参数,这个参数会表示 context 要更新成什麽值。assigner 可以是一个 object (推荐用法),用法如下:
import { Machine, assign } from "xstate";
// ...
actions: assign({
// 透过外部传进来的 event 来改变 count
count: (context, event) => context.count + event.value,
message: "value 也可以直接是 static value",
});
// ...
assigner 也可以是一个 function,用法如下:
// ...
// 他会 partial update context
actions: assign((context, event) => {
return {
count: context.count + event.value,
message: 'value 也可以直接是 static value'
}
}),
// ...
让我们直接来看一个简单的例子吧:
const counterMachine = Machine(
{
id: "counter",
initial: "ENABLED",
context: {
count: 0,
},
states: {
ENABLED: {
on: {
INC: {
actions: ["increment"],
},
DYNAMIC_INC: {
actions: ["dynamic_increment"],
},
RESET: {
actions: ["reset"],
},
DISABLE: "DISABLED",
},
},
DISABLED: {
on: {
ENABLE: "ENABLED",
},
},
},
},
{
actions: {
increment: assign({
count: (context) => context.count + 1,
}),
dynamic_increment: assign({
count: (context, event) => context.count + (event.value || 0),
}),
reset: assign({
count: 0,
}),
},
}
);
从上面这个范例,可以看出使用 XState 能够很清楚的定义出什麽状态下可以接收哪些 event,例如在 DISABLED 的状态下就只会对 ENABLE 的 event 会有反应,对于 INC, RESET 等事件就不会有反应。另外从 DYNAMIC_INC 事件可以看出如何根据外部传入的参数控制增长数值,详细可以参考以下这段,程序码:
//...
on: {
[COUNTER_EVENTS.DYNAMIC_INC]: {
actions: ['dynamic_increment'],
},
}
//...
actions: {
dynamic_increment: assign({
count: (context, event) => context.count + (event.value || 0)
// event 除了 type 这个属性之外有什麽 property 是外部决定的
}),
},
//...
//...
<Button
label="Increment"
onClick={() =>
// 这裡传入 DYNAMIC_INC event 同时要给 value
send({ type: COUNTER_EVENTS.DYNAMIC_INC, value: Number(value) })
}
/>
//...
注意事项
- 永远不要从外部修改一个 machine 内的 context,任何改变 context 的行为都应该来自 event。
- 推荐使用
assign({ ... })
的写法,这个写法利于未来的工具做分析。 - 跟所有 actions 相同不建议 inline 写在 machine 裡面,建议定义在 machine options 的 actions 内。
- 理想上,context 应该是一个 JS 的 plain object,并且应该可以被序列化。
- 记得
assign
就只是 pure function 回传一个 action 物件,并直接对 machine 造成影响。