- 动作 Actions
- API
- 动作顺序
- 发送动作(send action)
- 升高动作(raise action)
- ">响应动作 (respond action)
- ">转发动作(forwardTo action)
- ">错误升级动作(escalate action)
- 日志动作(log action)
- 纯动作(pure action)
- 自转换动作
动作 Actions
动作,是即发即弃的 作用。 它们可以通过三种方式声明:
entry动作,进入状态时执行exit动作,退出状态时执行- 执行转换时,执行转换的动作
要了解更多信息,请阅读 状态图简介中的动作。
API
可以像这样添加动作
```js {10-11,16-19,27-41} const triggerMachine = createMachine( { id: ‘trigger’, initial: ‘inactive’, states: { inactive: { on: { TRIGGER: { target: ‘active’, // 转换 actions actions: [‘activate’, ‘sendTelemetry’] } } }, active: { // 进入 actions entry: [‘notifyActive’, ‘sendTelemetry’], // 退出 actions exit: [‘notifyInactive’, ‘sendTelemetry’], on: { STOP: { target: ‘inactive’ } } } } }, { actions: { // action 实现 activate: (context, event) => { console.log(‘activating…’); }, notifyActive: (context, event) => { console.log(‘active!’); }, notifyInactive: (context, event) => { console.log(‘inactive!’); }, sendTelemetry: (context, event) => { console.log(‘time:’, Date.now()); } } } );
<details><summary>什么时候应该使用 转换 VS entry/exit 动作?</summary>这取决于! 它们的做的事不同:- entry/exit 操作,意味着“在进入/退出此状态的任何转换上 **执行此 动作**”。 当 动作 只依赖于它所在的状态节点,而不依赖于上一个/下一个状态节点 或 事件时,使用进入/退出 动作```js// ...{idle: {on: {LOAD: 'loading'}},loading: {// 每当进入“loading”状态时执行此 动作entry: 'fetchData'}}// ...
- 转换 动作 意味着“仅在此转换上 执行此 动作”。 当 动作 依赖于事件和它当前所处的状态节点时,使用转换 动作。
// ...{idle: {on: {LOAD: {target: 'loading',// 此 动作 仅在此转换时执行actions: 'fetchData'}},loading: {// ...}}// ...
::: tip 可以通过直接在状态机配置中指定 动作 函数来快速原型化 动作 实现:
```js {4} // … TRIGGER: { target: ‘active’, actions: (context, event) => { console.log(‘activating…’); } } // …
在状态机选项的 `actions` 属性中重构内联 动作 实现,可以更容易地调试、序列化、测试和准确地可视化 动作。:::## 声明动作从 `machine.transition(...)` 返回的 `State` 实例有一个 `.actions` 属性,它是一个供 解释(interpret) 执行的 动作 对象数组:```js {4-9}const activeState = triggerMachine.transition('inactive', { type: 'TRIGGER' });console.log(activeState.actions);// [// { type: 'activate', exec: ... },// { type: 'sendTelemetry', exec: ... },// { type: 'notifyActive', exec: ... },// { type: 'sendTelemetry', exec: ... }// ]
每个 动作 对象都有两个属性(以及其他可以指定的属性):
type- 动作 类型exec- 动作 执行函数
exec 函数有 3 个参数:
| 参数 | 类型 | 描述 |
|---|---|---|
context |
TContext | 当前状态机的上下文 |
event |
event object | 导致转换的事件 |
actionMeta |
meta object | 包含有关 动作 的元数据的对象(见下文) |
actionMeta 对象包括以下属性:
| 参数 | 类型 | 描述 |
|---|---|---|
action |
action object | 原始 动作 对象 |
state |
State | 转换后的已解析的状态机状态 |
解释(interpret)将调用带有 currentState.context、event 和状态机转换到的 state 的 exec 函数。 你可以自定义此 动作。 阅读 执行 动作 了解更多详情。
动作顺序
在执行状态图时,动作的顺序不一定重要(也就是说,它们不应该相互依赖)。 但是,state.actions 数组中的操作顺序是:
exit动作 - 退出状态节点的所有退出 动作,从原子状态节点开始- 转换
actions- 在所选转换上定义的所有 动作 entry动作 - 进入状态节点的所有进入 动作,从父状态开始
::: warning
在 XState 4.x 版中,assign 动作 具有优先权,并且在任何其他 动作 之前执行。 此行为将在第 5 版中修复,因为将按顺序调用 assign 操作。
:::
::: danger
此处记录的所有 动作 创建者都返回 动作 对象; 它是一个纯函数,它只返回一个 动作 对象,并 不是 命令式的发送一个事件。 不要命令式的调用 动作 创建者; 因为 他们什么都不会做!
// 🚫 不要这样做!entry: () => {// 🚫 这将什么也不做; send() 不是命令式函数!send({ type: 'SOME_EVENT' });};console.log(send({ type: 'SOME_EVENT' }));// => { type: 'xstate.send', event: { type: 'SOME_EVENT' } }// ✅ 这样替换entry: send({ type: 'SOME_EVENT' });
:::
发送动作(send action)
send(event) 动作 创建者创建了一个特殊的“发送” 动作 对象,它告诉服务(即,解释(interpret) 状态机)将该事件发送给它自己。 它在外部事件队列中,将一个事件排入正在运行的服务中,这意味着该事件将在 解释(interpret) 的下一步“步骤”上发送。
| 参数 | 类型 | 描述 |
|---|---|---|
event |
string or event object or event expression | 发送到指定options.to(或self)的事件 |
options? |
send options (见下文) | 发送事件的选项。 |
send options 参数是一个包含以下内容的对象:
| 参数 | 类型 | 描述 |
|---|---|---|
id? |
string | send ID (用于取消) |
to? |
string | 事件的目标(默认为self) |
delay? |
number | 发送事件前的超时时间(毫秒),如果在超时前没有取消事件 |
::: warning
send(...) 函数是一个 动作 创建者; 它是一个纯函数,它只返回一个 动作 对象,并 不会 命令式地发送一个事件。
:::
import { createMachine, send } from 'xstate';const lazyStubbornMachine = createMachine({id: 'stubborn',initial: 'inactive',states: {inactive: {on: {TOGGLE: {target: 'active',// 再次向服务发送 TOGGLE 事件actions: send('TOGGLE')}}},active: {on: {TOGGLE: { target: 'inactive' }}}}});const nextState = lazyStubbornMachine.transition('inactive', {type: 'TOGGLE'});nextState.value;// => 'active'nextState.actions;// => [{ type: 'xstate.send', event: { type: 'TOGGLE' }}]// 该服务将继续向自己发送 { type: 'TOGGLE' } 事件。
传递给 send(event) 的 event 参数可以是:
- 一个字符串事件,例如
send('TOGGLE') - 一个对象事件,例如
send({ type: 'TOGGLE', payload: ... }) - 一个事件表达式,它是一个函数,它接收触发
send()动作 的当前context和event,并返回一个事件对象:
import { send } from 'xstate';// 人为的例子 - 从 `context` 读取并发送动态创建的事件const sendName = send((context, event) => ({type: 'NAME',name: context.user.name}));const machine = createMachine({// ...on: {TOGGLE: {actions: sendName}}//...});
发送目标
从 send(...) 动作 创建者发送的事件,可以表示它应该发送到特定目标,例如 调用 服务 或 创建 演员。 这是通过在 send(...) 操作中指定 { to: ... } 属性来完成的:
// ...invoke: {id: 'some-service-id',src: 'someService',// ...},// ...// 表示向调用的服务发送 { type: 'SOME_EVENT' }actions: send({ type: 'SOME_EVENT' }, { to: 'some-service-id' })
to 属性中的 target 也可以是一个 target 表达式,它是一个函数,它接受当前触发动作的 context 和 event,并返回一个字符串 target 或一个 演员:
entry: assign({someActor: () => {return spawn(someMachine, 'some-actor-name');}}),// ...// 发送 { type: 'SOME_EVENT' } 到 演员 引用{actions: send({ type: 'SOME_EVENT' },{to: (context) => context.someActor})};
::: warning
同样,send(...) 函数是一个 动作 创建者,不会命令式发送事件。 相反,它返回一个 动作 对象,描述事件将发送到的位置:
console.log(send({ type: 'SOME_EVENT' }, { to: 'child' }));// logs:// {// type: 'xstate.send',// to: 'child',// event: {// type: 'SOME_EVENT'// }// }
:::
要从子状态机发送到父状态机,请使用 sendParent(event)(采用与 send(...) 相同的参数)。
升高动作(raise action)
raise() 动作 创建者在内部事件队列中,将一个事件排入状态图。 这意味着事件会在 解释(interpret) 的当前“步骤”上立即发送。
| 参数 | 类型 | 描述 |
|---|---|---|
event |
string or event object | 要提升的事件 |
import { createMachine, actions } from 'xstate';const { raise } = actions;const raiseActionDemo = createMachine({id: 'raisedmo',initial: 'entry',states: {entry: {on: {STEP: {target: 'middle'},RAISE: {target: 'middle',// 立即为“middle”调用 NEXT 事件actions: raise('NEXT')}}},middle: {on: {NEXT: { target: 'last' }}},last: {on: {RESET: { target: 'entry' }}}}});
单击 visualizer 中的“STEP”和“RAISE”事件以查看差异。
响应动作 (respond action)
respond() 动作 创建者创建一个 send() 动作,该 动作 被发送到,触发响应的事件的服务。
这在内部使用 SCXML 事件 ,从事件中获取 origin,并将 send() 动作 的 to 设置为 origin。
| 参数 | 类型 | 描述 |
|---|---|---|
event |
string, event object, or send expression | 发送回发件人的事件 |
options? |
send options object | 传递到 send() 事件的选项 |
使用响应 action 的示例
这演示了一些父服务(authClientMachine)向调用的 authServerMachine 发送一个 'CODE' 事件,并且 authServerMachine 响应一个 'TOKEN' 事件。
const authServerMachine = createMachine({initial: 'waitingForCode',states: {waitingForCode: {on: {CODE: {actions: respond({ type: 'TOKEN' }, { delay: 10 })}}}}});const authClientMachine = createMachine({initial: 'idle',states: {idle: {on: {AUTH: { target: 'authorizing' }}},authorizing: {invoke: {id: 'auth-server',src: authServerMachine},entry: send('CODE', { to: 'auth-server' }),on: {TOKEN: { target: 'authorized' }}},authorized: {type: 'final'}}});
详情请参阅 📖 发送响应。
转发动作(forwardTo action)
forwardTo() 动作 创建者,创建一个 send() 动作,通过其 ID 将最近的事件转发到指定的服务。
| 参数 | 类型 | 描述 |
|---|---|---|
target |
string or function that returns service | 要将最近事件发送到的目标服务。 |
使用 forwardTo 动作 的示例
import { createMachine, forwardTo, interpret } from 'xstate';function alertService(_, receive) {receive((event) => {if (event.type === 'ALERT') {alert(event.message);}});}const parentMachine = createMachine({id: 'parent',invoke: {id: 'alerter',src: () => alertService},on: {ALERT: { actions: forwardTo('alerter') }}});const parentService = interpret(parentMachine).start();parentService.send({ type: 'ALERT', message: 'hello world' });// => alerts "hello world"
错误升级动作(escalate action)
escalate() 动作 创建者,通过将错误发送到父状态机来升级错误。 这是作为状态机识别的特殊错误事件发送的。
| 参数 | 类型 | 描述 |
|---|---|---|
errorData |
any | 要升高(send)到父级的错误数据。 |
使用 escalate 动作 的示例
import { createMachine, actions } from 'xstate';const { escalate } = actions;const childMachine = createMachine({// ...// 这将被发送到调用这个孩子的父状态机entry: escalate({ message: 'This is some error' })});const parentMachine = createMachine({// ...invoke: {src: childMachine,onError: {actions: (context, event) => {console.log(event.data);// {// type: ...,// data: {// message: 'This is some error'// }// }}}}});
日志动作(log action)
log() 动作 创建器是一种记录与当前状态 context 和/或 event 相关的任何内容的声明方式。 它需要两个可选参数:
| 参数 | 类型 | 描述 |
|---|---|---|
expr? |
string or function | 一个简单的字符串或一个函数,它以 context 和 event 作为参数并返回一个要记录的值 |
label? |
string | 用于标记已记录消息的字符串 |
```js {9,14-17,28-34} import { createMachine, actions } from ‘xstate’; const { log } = actions;
const loggingMachine = createMachine({
id: ‘logging’,
context: { count: 42 },
initial: ‘start’,
states: {
start: {
entry: log(‘started!’),
on: {
FINISH: {
target: ‘end’,
actions: log(
(context, event) => count: ${context.count}, event: ${event.type},
‘Finish label’
)
}
}
},
end: {}
}
});
const endState = loggingMachine.transition(‘start’, ‘FINISH’);
endState.actions; // [ // { // type: ‘xstate.log’, // label: ‘Finish label’, // expr: (context, event) => … // } // ]
// interpreter 将根据当前状态上下文和事件记录 Action 的表达式。
没有任何参数,`log()` 是一个 动作,它记录一个具有 `context` 和 `event` 属性的对象,分别包含当前上下文和触发事件。## 选择动作(choose action)`choose()` 动作 创建者创建一个 动作,该 动作 指定应根据某些条件执行哪些 动作。| 参数 | 类型 | 描述 || -------- | ----- | ----------------------------------------------------------- || `conds` | array | 当给定的 `cond` 为真时,包含要执行的 `actions` 的对象数组(见下文) |**返回:**一个特殊的 `"xstate.choose"` 动作 对象,它在内部进行判断以有条件地确定应该执行哪些动作对象。`cond` 中的每个“条件动作”对象都具有以下属性:- `actions` - 要执行的 动作 对象- `cond?` - 执行这些 `actions` 的条件::: warning不要使用 `choose()` 动作 创建器来执行 动作,否则这些 动作 可能表示为通过 `entry`、`exit` 或 `actions` 在某些 状态/转换 中执行的非条件 动作。:::```jsimport { actions } from 'xstate';const { choose, log } = actions;const maybeDoThese = choose([{cond: 'cond1',actions: [// 当“cond1”为真时log('cond1 chosen!')]},{cond: 'cond2',actions: [// 当“cond1”为假且“cond2”为真时log((context, event) => {/* ... */}),log('another action')]},{cond: (context, event) => {// 一些条件return false;},actions: [// 当“cond1”和“cond2”为假并且内联`cond`为真时(context, event) => {// 一些其他 action}]},{actions: [log('fall-through action')// 当“cond1”、“cond2”和“cond3”为假时]}]);
这类似于 SCXML <if>、<elseif> 和 <else> 元素: www.w3.org/TR/scxml/#if
纯动作(pure action)
pure() 动作 创建器是一个纯函数(因此得名),它根据触发 动作 的当前状态“上下文”和“事件”返回要执行的 动作 对象。 这允许你动态定义应执行哪些 动作
| 参数 | 类型 | 描述 |
|---|---|---|
getActions |
function | 根据给定的 context 和 event 返回要执行的动作对象的函数(见下文) |
返回:
一个特殊的 "xstate.pure" 动作 对象,它将在内部判断 get 属性以确定应该执行的 动作 对象。
getActions(context, event) 参数:
| 参数 | 类型 | 描述 |
|---|---|---|
context |
object | 当前状态的 context |
event |
event object | 触发 动作 的事件对象 |
返回:
单个 动作 对象、一组 动作 对象或不代表任何 动作 对象的 undefined。
import { createMachine, actions } from 'xstate';const { pure } = actions;// 动态地向每个调用的示例 actor 发送一个事件const sendToAllSampleActors = pure((context, event) => {return context.sampleActors.map((sampleActor) => {return send('SOME_EVENT', { to: sampleActor });});});// => {// type: ActionTypes.Pure,// get: () => ... // 计算为 send() action 数组// }const machine = createMachine({// ...states: {active: {entry: sendToAllSampleActors}}});
自转换动作
自转换 是当状态转换到自身时,它 可能 退出然后重新进入自身。 自转换可以是 内部 或 外部 转换:
- 内部转换将 不 退出并重新进入自身,因此状态节点的“进入”和“退出”动作将不会再次执行。
- 内部转换用
{ internal: true }表示,或者将target保留为undefined。 - 将执行在转换的
actions属性上定义的 动作。
- 内部转换用
- 外部转换 将 退出并重新进入自身,因此状态节点的
entry和exitaction 将再次执行。- 默认情况下,所有转换都是外部的。 为了明确起见,你可以使用
{ internal: false }来指示它们。 - 将执行在转换的
actions属性上定义的 动作。
- 默认情况下,所有转换都是外部的。 为了明确起见,你可以使用
例如,这个计数器状态机,有一个带有内部和外部转换的 'counting' 状态:
```js {9-12} const counterMachine = createMachine({ id: ‘counter’, initial: ‘counting’, states: { counting: { entry: ‘enterCounting’, exit: ‘exitCounting’, on: { // 自转换 INC: { actions: ‘increment’ }, // 内部(隐式) DEC: { target: ‘counting’, actions: ‘decrement’ }, // 外部 DO_NOTHING: { internal: true, actions: ‘logNothing’ } // 内部(隐式) } } } });
// 外部转换(退出+ 转换action +进入) const stateA = counterMachine.transition(‘counting’, { type: ‘DEC’ }); stateA.actions; // [‘exitCounting’, ‘decrement’, ‘enterCounting’]
// 内部转换(转换动作) const stateB = counterMachine.transition(‘counting’, { type: ‘DO_NOTHING’ }); stateB.actions; // [‘logNothing’]
const stateC = counterMachine.transition(‘counting’, { type: ‘INC’ }); stateB.actions; // [‘increment’] ```
