演员 Actors
[[toc]]
演员(Actor)模型 是一种基于消息的计算的数学模型,它简化了多个“实体”(或“演员”)相互通信的方式。 演员通过相互发送消息(事件)来进行通信。 演员 的本地状态是私有的,除非它希望通过将其作为事件发送来与另一个 演员 共享。
当一个 演员 收到一个事件时,会发生三件事:
- 有限数量的消息可以 sent 给其他 演员
- 可以创建(或 spawned)有限数量的新演员
- 演员 的本地状态可能会改变(由其 behavior 决定)
状态机和状态图与 演员 模型配合得很好,因为它们是基于事件的行为和逻辑模型。 请记住:当状态机因事件而转换时,下一个状态包含:
- 下一个
value和context( 演员 的本地状态) - 要执行的下一个
actions(可能是新生成的 演员 或发送给其他 演员 的消息)
演员 可以是 创建的 或 调用的。 创建的 演员 与调用的 演员 有两个主要区别:
- 他们可以在任何时候被 创建(通过
assign(...)操作中的spawn(...)) - 他们可以随时 停止(通过
stop(...)动作)
演员 API
演员 XState 中实现)具有以下接口:
- 一个
id属性,它在本地系统中唯一标识角色 - 一个
.send(...)方法,用于向这个 演员 发送事件 - 一个
.getSnapshot()方法,同步返回演员的最后 触发值。
他们可能有可选的方法:
- 一个
.stop()方法,它停止 演员 并执行任何必要的清理 - Observable 的 演员 的
.subscribe(...)方法。
所有现有的调用服务模式都适合这个接口:
- 调用 promises 是忽略任何接收到的事件并最多将一个事件发送回父级的演员
- 调用 callbacks 是可以向父级发送事件的演员
(第一个
callback参数), 接收事件(第二个onReceive参数),并对它们采取动作 - 调用 machines 是可以将事件发送到父级(
sendParent(...)动作)或它引用的其他演员(send(...)动作)、接收事件、对它们采取行动(状态转换和动作)的 演员 ),产生新的 演员(spawn(...)函数),并停止 演员。 - 调用 observables 是其发出的值表示要发送回父级的事件的 演员。
::: tip 什么是触发的值?
演员的 触发值 是订阅者在 演员 的.subscribe(...) 方法中收到的值。
- 对于 service,发出当前状态。
- 对于 promise,发出 resolve 的值(如果未实现,则为“未定义”)。
- 对于 observables,发出最新发出的值。
- 对于 callback,不会发出任何内容。
:::
创建演员
就像在基于 演员 模型的语言中一样 Akka 或 Erlang,演员 在context 中产生并被引用(作为assign(...) 操作的结果)。
- 从
'xstate'导入spawn函数 - 在
assign(...)动作中,使用spawn(...)创建一个新的 演员 引用
spawn(...) 函数通过提供 1 或 2 个参数来创建 演员 引用:
entity- 代表 演员 东走的(反应)值或状态机。entity的可能类型:name(可选) - 唯一标识 演员 的字符串。 这对于所有生成的 演员 和调用的服务应该是唯一的。
或者,spawn 接受一个选项对象作为第二个参数,它可能包含以下选项:
name(可选) - 唯一标识演员的字符串。 这对于所有生成的演员和调用的服务应该是唯一的。autoForward- (可选)true如果发送到这台状态机的所有事件也应该发送(或 转发)到被调用的子节点(默认情况下为false)sync- (可选)true如果这台状态机应该自动订阅产生的子状态机的状态,状态将在子状态机 ref 上存储为.state
```js {13-14} import { createMachine, spawn } from ‘xstate’; import { todoMachine } from ‘./todoMachine’;
const todosMachine = createMachine({
// …
on: {
‘NEW_TODO.ADD’: {
actions: assign({
todos: (context, event) => [
…context.todos,
{
todo: event.todo,
// 添加一个具有唯一名称的新 todoMachine actor
ref: spawn(todoMachine, todo-${event.id})
}
]
})
}
// …
}
});
如果你没有为 `spawn(...)` 提供 `name` 参数,将会自动生成一个唯一的名称。 此名称将是不确定的⚠️。::: tip将 `const actorRef = spawn(someMachine)` 视为 `context` 中的一个普通值。 你可以根据你的逻辑要求将此 `actorRef` 放置在 `context` 内的任何位置。 只要它在`assign(...)` 中的赋值函数内,它就会被限定在它产生的服务范围内。:::::: warning不要在赋值函数之外调用 `spawn(...)`。 这将产生一个没有效果的孤儿演员(没有父级)。```js// ❌ 永远不要在外部调用 spawn(...)const someActorRef = spawn(someMachine);// ❌ spawn(...) 不是action创建者{actions: spawn(someMachine);}// ❌ 不要在赋值函数之外赋值 spawn(...){actions: assign({// 记住:这是在服务启动之前立即调用的someActorRef: spawn(someMachine)});}// ✅ 在赋值函数中分配 spawn(...){actions: assign({someActorRef: () => spawn(someMachine)});}
:::
可以生成不同类型的值作为演员。
发送事件到演员
使用 send() 动作,事件可以通过 目标表达式 发送给演员:
```js {13} const machine = createMachine({ // … states: { active: { entry: assign({ someRef: () => spawn(someMachine) }), on: { SOME_EVENT: { // 使用目标表达式将事件发送到演员引用 actions: send({ type: ‘PING’ }, { to: (context) => context.someRef }) } } } } });
::: tip如果你为 `spawn(...)` 提供了一个唯一的 `name` 参数,你可以在目标表达式中引用它:```jsconst loginMachine = createMachine({// ...entry: assign({formRef: () => spawn(formMachine, 'form')}),states: {idle: {on: {LOGIN: {actions: send({ type: 'SUBMIT' }, { to: 'form' })}}}}});
:::
停止演员
使用 stop(...) 动作创建器停止演员:
const someMachine = createMachine({// ...entry: [// 通过引用停止一个actorstop((context) => context.someActorRef),// 通过 ID 停止 actorstop('some-actor')]});
创建 Promises
就像 invoking promises 一样,promise 可以作为 演员 生成。 发送回状态机的事件将是一个 'done.invoke.<ID>' 操作,promise 响应作为有效负载中的 data 属性:
``js {11}
// Returns a promise
const fetchData = (query) => {
return fetch(http://example.com?query=${event.query}`).then((data) =>
data.json()
);
};
// … { actions: assign({ ref: (_, event) => spawn(fetchData(event.query)) }); } // …
::: warning不建议生成promise 演员,因为 [调用 promises]($zh-guides-communication.md#invoking-promises) 是一种更好的模式,因为它们依赖于状态(自我取消)并且具有更可预测的行为。:::## 创建 Callbacks就像 [调用 callbacks]($zh-guides-communication.md#invoking-callbacks) 一样,回调可以作为 演员 产生。 这个例子模拟了一个 定时计数 演员,它每秒增加自己的计数,但也可以对 `{ type: 'INC' }` 事件做出反应。```js {22}const counterInterval = (callback, receive) => {let count = 0;const intervalId = setInterval(() => {callback({ type: 'COUNT.UPDATE', count });count++;}, 1000);receive(event => {if (event.type === 'INC') {count++;}});return () => { clearInterval(intervalId); }}const machine = createMachine({// ...{actions: assign({counterRef: () => spawn(counterInterval)})}// ...});
然后可以将事件发送给演员:
```js {5-7} const machine = createMachine({ // … on: { ‘COUNTER.INC’: { actions: send({ type: ‘INC’ }, { to: (context) => context.counterRef }) } } // … });
## 创建 Observables就像 [调用 observables]($zh-guides-communication.md#invoking-observables) 一样,observables 可以作为 演员 生成:```js {22}import { interval } from 'rxjs';import { map } from 'rxjs/operators';const createCounterObservable = (ms) => interval(ms).pipe(map(count => ({ type: 'COUNT.UPDATE', count })))const machine = createMachine({context: { ms: 1000 },// ...{actions: assign({counterRef: ({ ms }) => spawn(createCounterObservable(ms))})}// ...on: {'COUNT.UPDATE': { /* ... */ }}});
创建状态机
状态机是使用 演员 的最有效方式,因为它们提供了最多的功能。 生成状态机就像 调用 状态机,其中一个 machine 被传递到 spawn(machine):
```js {13,26,30-32} const remoteMachine = createMachine({ id: ‘remote’, initial: ‘offline’, states: { offline: { on: { WAKE: ‘online’ } }, online: { after: { 1000: { actions: sendParent(‘REMOTE.ONLINE’) } } } } });
const parentMachine = createMachine({ id: ‘parent’, initial: ‘waiting’, context: { localOne: null }, states: { waiting: { entry: assign({ localOne: () => spawn(remoteMachine) }), on: { ‘LOCAL.WAKE’: { actions: send({ type: ‘WAKE’ }, { to: (context) => context.localOne }) }, ‘REMOTE.ONLINE’: { target: ‘connected’ } } }, connected: {} } });
const parentService = interpret(parentMachine) .onTransition((state) => console.log(state.value)) .start();
parentService.send({ type: ‘LOCAL.WAKE’ }); // => ‘waiting’ // … after 1000ms // => ‘connected’
## 同步和读取 State <Badge text="4.6.1+"/>演员 模型的主要原则之一是, 演员 状态是 _私有的_ 和 _本地的_,它永远不会共享,除非 演员 选择通过消息传递来共享它。 坚持使用这个模型,演员 可以在其状态发生变化时,通过向其发送具有最新状态的特殊“更新”事件,来 _通知_ 其父级。 换句话说,父演员 可以订阅其子演员 的状态。为此,请将 `{ sync: true }` 设置为 `spawn(...)` 的选项:```js {4}// ...{actions: assign({// 每当其状态发生变化时,Actor 都会向父级发送更新事件someRef: () => spawn(todoMachine, { sync: true })});}// ...
这将自动为状态机订阅生成的子状态机的状态,该状态会保持更新并可通过 getSnapshot() 访问:
someService.onTransition((state) => {const { someRef } = state.context;console.log(someRef.getSnapshot());// => State {// value: ...,// context: ...// }});
someService.onTransition((state) => {const { someRef } = state.context;console.log(someRef.state);// => State {// value: ...,// context: ...// }});
::: warning
默认情况下,sync 设置为 false。 当禁用sync时,永远不要读取演员的.state; 否则,你最终将引用陈旧的状态。
:::
发送更新
对于不与父级同步的 演员,演员 可以通过 sendUpdate() 向其父状态机发送显式事件:
import { createMachine, sendUpdate } from 'xstate';const childMachine = createMachine({// ...on: {SOME_EVENT: {actions: [// ...// 创建一个将更新事件发送给父级的操作sendUpdate()]}}});
::: tip
更喜欢显式地向父级发送事件(sendUpdate()),而不是订阅每个状态更改。 与生成的状态机同步可能会导致“闲聊”事件日志,因为来自子级的每次更新都会导致从子级发送到父级的新“xstate.update”事件。
:::
快速参考
导入 spawn 并创建演员:
import { spawn } from 'xstate';
在 assign 动作中 创建演员 :
// ...{actions: assign({someRef: (context, event) => spawn(someMachine)});}// ...
创建不同类型 的演员:
// ...{actions: assign({// 来自 promisepromiseRef: (context, event) =>spawn(new Promise((resolve, reject) => {// ...}),'my-promise'),// 来自callbackcallbackRef: (context, event) =>spawn((callback, receive) => {// 发送到父级callback('SOME_EVENT');// 接收父级receive((event) => {// 处理 event});// 处理return () => {/* 在这里做清理 */};}),// 来自 observableobservableRef: (context, event) => spawn(someEvent$),// 来自machinemachineRef: (context, event) =>spawn(createMachine({// ...}))});}// ...
与 演员 同步状态:
// ...{actions: assign({someRef: () => spawn(someMachine, { sync: true })});}// ...
从 演员 那里 获取快照:
service.onTransition((state) => {const { someRef } = state.context;someRef.getSnapshot();// => State { ... }});
使用 send 动作创建者向演员发送事件:
// ...{actions: send({ type: 'SOME_EVENT' },{to: (context) => context.someRef});}// ...
使用 send 表达式 将带有数据的事件发送给演员:
// ...{actions: send((context, event) => ({ ...event, type: 'SOME_EVENT' }), {to: (context) => context.someRef});}// ...
使用 sendParent 动作创建者将事件从演员发送到父级:
// ...{actions: sendParent({ type: 'ANOTHER_EVENT' });}// ...
使用 sendParent 表达式将带有数据的事件从演员发送到父级:
// ...{actions: sendParent((context, event) => ({...context,type: 'ANOTHER_EVENT'}));}// ...
从 context 查看演员 :
someService.onTransition((state) => {const { someRef } = state.context;console.log(someRef);// => { id: ..., send: ... }});
