演员 Actors

:rocket: 快速参考

[[toc]]

演员(Actor)模型 是一种基于消息的计算的数学模型,它简化了多个“实体”(或“演员”)相互通信的方式。 演员通过相互发送消息(事件)来进行通信。 演员 的本地状态是私有的,除非它希望通过将其作为事件发送来与另一个 演员 共享。

当一个 演员 收到一个事件时,会发生三件事:

  • 有限数量的消息可以 sent 给其他 演员
  • 可以创建(或 spawned)有限数量的新演员
  • 演员 的本地状态可能会改变(由其 behavior 决定)

状态机和状态图与 演员 模型配合得很好,因为它们是基于事件的行为和逻辑模型。 请记住:当状态机因事件而转换时,下一个状态包含:

  • 下一个 valuecontext( 演员 的本地状态)
  • 要执行的下一个 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,不会发出任何内容。

:::

创建演员

就像在基于 演员 模型的语言中一样 AkkaErlang,演员 在context 中产生并被引用(作为assign(...) 操作的结果)。

  1. 'xstate'导入spawn函数
  2. 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}) } ] }) } // … } });

  1. 如果你没有为 `spawn(...)` 提供 `name` 参数,将会自动生成一个唯一的名称。 此名称将是不确定的⚠️。
  2. ::: tip
  3. `const actorRef = spawn(someMachine)` 视为 `context` 中的一个普通值。 你可以根据你的逻辑要求将此 `actorRef` 放置在 `context` 内的任何位置。 只要它在`assign(...)` 中的赋值函数内,它就会被限定在它产生的服务范围内。
  4. :::
  5. ::: warning
  6. 不要在赋值函数之外调用 `spawn(...)` 这将产生一个没有效果的孤儿演员(没有父级)。
  7. ```js
  8. // ❌ 永远不要在外部调用 spawn(...)
  9. const someActorRef = spawn(someMachine);
  10. // ❌ spawn(...) 不是action创建者
  11. {
  12. actions: spawn(someMachine);
  13. }
  14. // ❌ 不要在赋值函数之外赋值 spawn(...)
  15. {
  16. actions: assign({
  17. // 记住:这是在服务启动之前立即调用的
  18. someActorRef: spawn(someMachine)
  19. });
  20. }
  21. // ✅ 在赋值函数中分配 spawn(...)
  22. {
  23. actions: assign({
  24. someActorRef: () => spawn(someMachine)
  25. });
  26. }

:::

可以生成不同类型的值作为演员。

发送事件到演员

使用 send() 动作,事件可以通过 目标表达式 发送给演员:

```js {13} const machine = createMachine({ // … states: { active: { entry: assign({ someRef: () => spawn(someMachine) }), on: { SOME_EVENT: { // 使用目标表达式将事件发送到演员引用 actions: send({ type: ‘PING’ }, { to: (context) => context.someRef }) } } } } });

  1. ::: tip
  2. 如果你为 `spawn(...)` 提供了一个唯一的 `name` 参数,你可以在目标表达式中引用它:
  3. ```js
  4. const loginMachine = createMachine({
  5. // ...
  6. entry: assign({
  7. formRef: () => spawn(formMachine, 'form')
  8. }),
  9. states: {
  10. idle: {
  11. on: {
  12. LOGIN: {
  13. actions: send({ type: 'SUBMIT' }, { to: 'form' })
  14. }
  15. }
  16. }
  17. }
  18. });

:::

停止演员

使用 stop(...) 动作创建器停止演员:

  1. const someMachine = createMachine({
  2. // ...
  3. entry: [
  4. // 通过引用停止一个actor
  5. stop((context) => context.someActorRef),
  6. // 通过 ID 停止 actor
  7. stop('some-actor')
  8. ]
  9. });

创建 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)) }); } // …

  1. ::: warning
  2. 不建议生成promise 演员,因为 [调用 promises]($zh-guides-communication.md#invoking-promises) 是一种更好的模式,因为它们依赖于状态(自我取消)并且具有更可预测的行为。
  3. :::
  4. ## 创建 Callbacks
  5. 就像 [调用 callbacks]($zh-guides-communication.md#invoking-callbacks) 一样,回调可以作为 演员 产生。 这个例子模拟了一个 定时计数 演员,它每秒增加自己的计数,但也可以对 `{ type: 'INC' }` 事件做出反应。
  6. ```js {22}
  7. const counterInterval = (callback, receive) => {
  8. let count = 0;
  9. const intervalId = setInterval(() => {
  10. callback({ type: 'COUNT.UPDATE', count });
  11. count++;
  12. }, 1000);
  13. receive(event => {
  14. if (event.type === 'INC') {
  15. count++;
  16. }
  17. });
  18. return () => { clearInterval(intervalId); }
  19. }
  20. const machine = createMachine({
  21. // ...
  22. {
  23. actions: assign({
  24. counterRef: () => spawn(counterInterval)
  25. })
  26. }
  27. // ...
  28. });

然后可以将事件发送给演员:

```js {5-7} const machine = createMachine({ // … on: { ‘COUNTER.INC’: { actions: send({ type: ‘INC’ }, { to: (context) => context.counterRef }) } } // … });

  1. ## 创建 Observables
  2. 就像 [调用 observables]($zh-guides-communication.md#invoking-observables) 一样,observables 可以作为 演员 生成:
  3. ```js {22}
  4. import { interval } from 'rxjs';
  5. import { map } from 'rxjs/operators';
  6. const createCounterObservable = (ms) => interval(ms)
  7. .pipe(map(count => ({ type: 'COUNT.UPDATE', count })))
  8. const machine = createMachine({
  9. context: { ms: 1000 },
  10. // ...
  11. {
  12. actions: assign({
  13. counterRef: ({ ms }) => spawn(createCounterObservable(ms))
  14. })
  15. }
  16. // ...
  17. on: {
  18. 'COUNT.UPDATE': { /* ... */ }
  19. }
  20. });

创建状态机

状态机是使用 演员 的最有效方式,因为它们提供了最多的功能。 生成状态机就像 调用 状态机,其中一个 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’

  1. ## 同步和读取 State <Badge text="4.6.1+"/>
  2. 演员 模型的主要原则之一是, 演员 状态是 _私有的_ _本地的_,它永远不会共享,除非 演员 选择通过消息传递来共享它。 坚持使用这个模型,演员 可以在其状态发生变化时,通过向其发送具有最新状态的特殊“更新”事件,来 _通知_ 其父级。 换句话说,父演员 可以订阅其子演员 的状态。
  3. 为此,请将 `{ sync: true }` 设置为 `spawn(...)` 的选项:
  4. ```js {4}
  5. // ...
  6. {
  7. actions: assign({
  8. // 每当其状态发生变化时,Actor 都会向父级发送更新事件
  9. someRef: () => spawn(todoMachine, { sync: true })
  10. });
  11. }
  12. // ...

这将自动为状态机订阅生成的子状态机的状态,该状态会保持更新并可通过 getSnapshot() 访问:

  1. someService.onTransition((state) => {
  2. const { someRef } = state.context;
  3. console.log(someRef.getSnapshot());
  4. // => State {
  5. // value: ...,
  6. // context: ...
  7. // }
  8. });
  1. someService.onTransition((state) => {
  2. const { someRef } = state.context;
  3. console.log(someRef.state);
  4. // => State {
  5. // value: ...,
  6. // context: ...
  7. // }
  8. });

::: warning 默认情况下,sync 设置为 false。 当禁用sync时,永远不要读取演员的.state; 否则,你最终将引用陈旧的状态。 :::

发送更新

对于不与父级同步的 演员,演员 可以通过 sendUpdate() 向其父状态机发送显式事件:

  1. import { createMachine, sendUpdate } from 'xstate';
  2. const childMachine = createMachine({
  3. // ...
  4. on: {
  5. SOME_EVENT: {
  6. actions: [
  7. // ...
  8. // 创建一个将更新事件发送给父级的操作
  9. sendUpdate()
  10. ]
  11. }
  12. }
  13. });

::: tip 更喜欢显式地向父级发送事件(sendUpdate()),而不是订阅每个状态更改。 与生成的状态机同步可能会导致“闲聊”事件日志,因为来自子级的每次更新都会导致从子级发送到父级的新“xstate.update”事件。 :::

快速参考

导入 spawn 并创建演员:

  1. import { spawn } from 'xstate';

assign 动作中 创建演员

  1. // ...
  2. {
  3. actions: assign({
  4. someRef: (context, event) => spawn(someMachine)
  5. });
  6. }
  7. // ...

创建不同类型 的演员:

  1. // ...
  2. {
  3. actions: assign({
  4. // 来自 promise
  5. promiseRef: (context, event) =>
  6. spawn(
  7. new Promise((resolve, reject) => {
  8. // ...
  9. }),
  10. 'my-promise'
  11. ),
  12. // 来自callback
  13. callbackRef: (context, event) =>
  14. spawn((callback, receive) => {
  15. // 发送到父级
  16. callback('SOME_EVENT');
  17. // 接收父级
  18. receive((event) => {
  19. // 处理 event
  20. });
  21. // 处理
  22. return () => {
  23. /* 在这里做清理 */
  24. };
  25. }),
  26. // 来自 observable
  27. observableRef: (context, event) => spawn(someEvent$),
  28. // 来自machine
  29. machineRef: (context, event) =>
  30. spawn(
  31. createMachine({
  32. // ...
  33. })
  34. )
  35. });
  36. }
  37. // ...

与 演员 同步状态:

  1. // ...
  2. {
  3. actions: assign({
  4. someRef: () => spawn(someMachine, { sync: true })
  5. });
  6. }
  7. // ...

从 演员 那里 获取快照

  1. service.onTransition((state) => {
  2. const { someRef } = state.context;
  3. someRef.getSnapshot();
  4. // => State { ... }
  5. });

使用 send 动作创建者向演员发送事件

  1. // ...
  2. {
  3. actions: send(
  4. { type: 'SOME_EVENT' },
  5. {
  6. to: (context) => context.someRef
  7. }
  8. );
  9. }
  10. // ...

使用 send 表达式 将带有数据的事件发送给演员

  1. // ...
  2. {
  3. actions: send((context, event) => ({ ...event, type: 'SOME_EVENT' }), {
  4. to: (context) => context.someRef
  5. });
  6. }
  7. // ...

使用 sendParent 动作创建者将事件从演员发送到父级:

  1. // ...
  2. {
  3. actions: sendParent({ type: 'ANOTHER_EVENT' });
  4. }
  5. // ...

使用 sendParent 表达式将带有数据的事件从演员发送到父级:

  1. // ...
  2. {
  3. actions: sendParent((context, event) => ({
  4. ...context,
  5. type: 'ANOTHER_EVENT'
  6. }));
  7. }
  8. // ...

context 查看演员

  1. someService.onTransition((state) => {
  2. const { someRef } = state.context;
  3. console.log(someRef);
  4. // => { id: ..., send: ... }
  5. });