调用 Services

:rocket: 快速参考

在一台状态机上表达整个应用程序的行为很快就会变得复杂和笨拙。 使用多个相互通信的状态机来表达复杂的逻辑是很自然的(并且受到鼓励!)。 这非常类似于 演员(Actor),其中每个状态机实例都被视为一个“演员”,可以向其他“演员”(例如 Promise 或其他状态机)发送和接收事件(消息)并对其做出响应 .

为了让状态机相互通信,父状态机调用子状态机并通过监听子状态机sendParent(...)发送的事件,或者等待子状态机到达其最终状态 ,这将导致进行onDone转换。

你可以调用:

  • Promises, 这将在 resolve 上采用 onDone 转换,或者在 reject 上采用 onError 转换
  • Callbacks, 它可以向父状态机发送事件和从父状态机接收事件
  • Observables, 它可以向父状态机发送事件,以及完成时的信号
  • Machines, 它还可以发送/接收事件,并在达到最终状态时通知父状态机

invoke 属性

调用是在状态节点的配置中使用 invoke 属性定义的,其值是一个包含以下内容的对象:

  • src - 要调用的服务的来源,可以是:
    • 状态机
    • 一个返回 Promise 的函数
    • 一个返回“回调处理程序”的函数
    • 返回可观察的函数
    • 一个字符串,它指的是在这台状态机的 options.services 中定义的 4 个列出的选项中的任何一个
    • 一个调用源对象 ,它包含 { type: src } 中的源字符串,以及任何其他元数据。
  • id - 被调用服务的唯一标识符
  • onDone - (可选)在以下情况下采用的 转换
    • 子状态机达到其 最终状态,或
    • 调用的 Promise 的 resolve,或
    • 被调用的 observable 完成
  • onError - (可选)当被调用的服务遇到执行错误时要进行的转换。
  • autoForward - (可选)true 如果发送到这台状态机的所有事件也应该发送(或 forwarded)到被调用的子节点(默认情况下为 false
    • ⚠️ 避免将 autoForward 设置为 true,因为盲目转发所有事件可能会导致意外行为和/或无限循环。 总是更喜欢显式发送事件,和/或使用 forward(...) 动作创建者直接将事件转发给被调用的孩子。 (目前仅适用于状态机!⚠️)
  • data - (可选,仅在调用状态机时使用)将子状态机的 context 的属性映射到从父状态机的 context 返回相应值的函数的对象。

::: warning 不要将状态的 onDone 属性与 invoke.onDone 混淆——它们是类似的转换,它们指的是不同的东西。

  • 状态节点 上的onDone 属性指的是复合状态节点到达最终状态
  • invoke.onDone 属性指的是正在完成的调用 (invoke.src)。

``js {5,13} // ... loading: { invoke: { src: someSrc, onDone: {/* ... */} // 指的是someSrc` 正在完成 }, initial: ‘loadFoo’, states: { loadFoo: {//}, loadBar: {//}, loadingComplete: { type: ‘final’ } }, onDone: ‘loaded’ // 指的是达到“loading.loadingComplete” } // …

  1. :::
  2. ## 调用 Promises
  3. 由于每个 Promise 都可以建模为状态机,因此 XState 可以按原样调用 Promise Promise 可以:
  4. - `resolve()`, 这将采取`onDone`转换
  5. - `reject()` (或抛出错误),这将采用 `onError` 转换
  6. 如果在 Promise resolve 之前,退出被调用的 Promise 处于活动状态的状态,则 Promise 的结果将被丢弃。
  7. ```js
  8. // Function 返回 promise
  9. // 这个 promise 可能会 resolve,例如,
  10. // { name: 'David', location: 'Florida' }
  11. const fetchUser = (userId) =>
  12. fetch(`url/to/user/${userId}`).then((response) => response.json());
  13. const userMachine = createMachine({
  14. id: 'user',
  15. initial: 'idle',
  16. context: {
  17. userId: 42,
  18. user: undefined,
  19. error: undefined
  20. },
  21. states: {
  22. idle: {
  23. on: {
  24. FETCH: { target: 'loading' }
  25. }
  26. },
  27. loading: {
  28. invoke: {
  29. id: 'getUser',
  30. src: (context, event) => fetchUser(context.userId),
  31. onDone: {
  32. target: 'success',
  33. actions: assign({ user: (context, event) => event.data })
  34. },
  35. onError: {
  36. target: 'failure',
  37. actions: assign({ error: (context, event) => event.data })
  38. }
  39. }
  40. },
  41. success: {},
  42. failure: {
  43. on: {
  44. RETRY: { target: 'loading' }
  45. }
  46. }
  47. }
  48. });

解析后的数据被放置在一个 'done.invoke.<id>' 事件中,在 data 属性下,例如:

  1. {
  2. type: 'done.invoke.getUser',
  3. data: {
  4. name: 'David',
  5. location: 'Florida'
  6. }
  7. }

Promise Rejection

如果 Promise 拒绝,则将使用 { type: 'error.platform' } 事件进行 onError 转换。 错误数据在事件的 data 属性中可用:

  1. const search = (context, event) => new Promise((resolve, reject) => {
  2. if (!event.query.length) {
  3. return reject('No query specified');
  4. // or:
  5. // throw new Error('No query specified');
  6. }
  7. return resolve(getSearchResults(event.query));
  8. });
  9. // ...
  10. const searchMachine = createMachine({
  11. id: 'search',
  12. initial: 'idle',
  13. context: {
  14. results: undefined,
  15. errorMessage: undefined,
  16. },
  17. states: {
  18. idle: {
  19. on: {
  20. SEARCH: { target: 'searching' }
  21. }
  22. },
  23. searching: {
  24. invoke: {
  25. id: 'search'
  26. src: search,
  27. onError: {
  28. target: 'failure',
  29. actions: assign({
  30. errorMessage: (context, event) => {
  31. // event is:
  32. // { type: 'error.platform', data: 'No query specified' }
  33. return event.data;
  34. }
  35. })
  36. },
  37. onDone: {
  38. target: 'success',
  39. actions: assign({ results: (_, event) => event.data })
  40. }
  41. }
  42. },
  43. success: {},
  44. failure: {}
  45. }
  46. });

::: warning

如果缺少 onError 转换并且 Promise 被拒绝,则错误将被忽略,除非你为状态机指定了 严格模式。 在这种情况下,严格模式将停止状态机并抛出错误。

:::

调用 Callbacks

发送到父状态机的事件流可以通过回调处理程序建模,这是一个接受两个参数的函数:

返回值(可选)应该是在退出当前状态时,对调用的服务执行清理(即取消订阅、防止内存泄漏等)的函数。 回调 不能 使用 async/await 语法,因为它会自动将返回值包装在 Promise 中。

  1. // ...
  2. counting: {
  3. invoke: {
  4. id: 'incInterval',
  5. src: (context, event) => (callback, onReceive) => {
  6. // 这将每秒向父级发送 'INC' 事件
  7. const id = setInterval(() => callback('INC'), 1000);
  8. // 执行清理
  9. return () => clearInterval(id);
  10. }
  11. },
  12. on: {
  13. INC: { actions: assign({ counter: context => context.counter + 1 }) }
  14. }
  15. }
  16. // ...

收听父级事件

被调用的回调处理程序还被赋予了第二个参数 onReceive,它为从父级发送到回调处理程序的事件注册监听器。 这允许在父状态机和调用的回调服务之间进行父子通信。

例如,父状态机向子 'ponger' 服务发送 'PING' 事件。 子服务可以使用 onReceive(listener) 监听该事件,并将一个 'PONG' 事件发送回父级作为响应:

  1. const pingPongMachine = createMachine({
  2. id: 'pinger',
  3. initial: 'active',
  4. states: {
  5. active: {
  6. invoke: {
  7. id: 'ponger',
  8. src: (context, event) => (callback, onReceive) => {
  9. // 每当父级发送“PING”时,
  10. // 发送父'PONG'事件
  11. onReceive((e) => {
  12. if (e.type === 'PING') {
  13. callback('PONG');
  14. }
  15. });
  16. }
  17. },
  18. entry: send({ type: 'PING' }, { to: 'ponger' }),
  19. on: {
  20. PONG: { target: 'done' }
  21. }
  22. },
  23. done: {
  24. type: 'final'
  25. }
  26. }
  27. });
  28. interpret(pingPongMachine)
  29. .onDone(() => done())
  30. .start();

调用 Observables

Observables 是随时间发出的值流。 将它们视为一个数组/集合,其值是异步发出的,而不是一次发出。 JavaScript 中有许多 observable 的实现; 最受欢迎的是 RxJS

可以调用 Observables,它应该向父状态机发送事件(字符串或对象),但不接收事件(单向)。 一个 observable 调用是一个函数,它以 contextevent 作为参数并返回一个可观察的事件流。 当退出调用它的状态时,observable 被取消订阅。

  1. import { createMachine, interpret } from 'xstate';
  2. import { interval } from 'rxjs';
  3. import { map, take } from 'rxjs/operators';
  4. const intervalMachine = createMachine({
  5. id: 'interval',
  6. initial: 'counting',
  7. context: { myInterval: 1000 },
  8. states: {
  9. counting: {
  10. invoke: {
  11. src: (context, event) =>
  12. interval(context.myInterval).pipe(
  13. map((value) => ({ type: 'COUNT', value })),
  14. take(5)
  15. ),
  16. onDone: 'finished'
  17. },
  18. on: {
  19. COUNT: { actions: 'notifyCount' },
  20. CANCEL: { target: 'finished' }
  21. }
  22. },
  23. finished: {
  24. type: 'final'
  25. }
  26. }
  27. });

上面的intervalMachine 将接收来自interval(...) 映射到事件对象的事件,直到可观察对象“完成”(完成发射值)。 如果 "CANCEL" 事件发生,observable 将被处理(.unsubscribe() 将在内部调用)。

::: tip 不一定需要为每次调用都创建 Observable。 可以改为引用“热可观察”:

  1. import { fromEvent } from 'rxjs';
  2. const mouseMove$ = fromEvent(document.body, 'mousemove');
  3. const mouseMachine = createMachine({
  4. id: 'mouse',
  5. // ...
  6. invoke: {
  7. src: (context, event) => mouseMove$
  8. },
  9. on: {
  10. mousemove: {
  11. /* ... */
  12. }
  13. }
  14. });

:::

调用 Machines

状态机分层通信,被调用的状态机可以通信:

  • 通过send(EVENT, { to: 'someChildId' }) 动作实现父到子
  • 通过 sendParent(EVENT) 操作实现子级到父级。

如果退出调用状态机的状态,则状态机停止。

  1. import { createMachine, interpret, send, sendParent } from 'xstate';
  2. // 调用子状态机
  3. const minuteMachine = createMachine({
  4. id: 'timer',
  5. initial: 'active',
  6. states: {
  7. active: {
  8. after: {
  9. 60000: { target: 'finished' }
  10. }
  11. },
  12. finished: { type: 'final' }
  13. }
  14. });
  15. const parentMachine = createMachine({
  16. id: 'parent',
  17. initial: 'pending',
  18. states: {
  19. pending: {
  20. invoke: {
  21. src: minuteMachine,
  22. // 当 minuteMachine 达到其顶级最终状态时,将进行 onDone 转换。
  23. onDone: 'timesUp'
  24. }
  25. },
  26. timesUp: {
  27. type: 'final'
  28. }
  29. }
  30. });
  31. const service = interpret(parentMachine)
  32. .onTransition((state) => console.log(state.value))
  33. .start();
  34. // => 'pending'
  35. // ... after 1 minute
  36. // => 'timesUp'

使用 Context 调用

子状态机可以使用从父状态机的 contextdata 属性派生的 context 调用。 例如,下面的parentMachine 将调用一个新的timerMachine 服务,初始上下文为{duration: 3000 }

  1. const timerMachine = createMachine({
  2. id: 'timer',
  3. context: {
  4. duration: 1000 // 默认 duration
  5. }
  6. /* ... */
  7. });
  8. const parentMachine = createMachine({
  9. id: 'parent',
  10. initial: 'active',
  11. context: {
  12. customDuration: 3000
  13. },
  14. states: {
  15. active: {
  16. invoke: {
  17. id: 'timer',
  18. src: timerMachine,
  19. // 从父上下文 派生子上下文
  20. data: {
  21. duration: (context, event) => context.customDuration
  22. }
  23. }
  24. }
  25. }
  26. });

就像 assign(...) 一样,子上下文可以映射为对象(首选)或函数:

  1. // 对象(每个属性):
  2. data: {
  3. duration: (context, event) => context.customDuration,
  4. foo: (context, event) => event.value,
  5. bar: 'static value'
  6. }
  7. // 函数(聚合),相当于上面的:
  8. data: (context, event) => ({
  9. duration: context.customDuration,
  10. foo: event.value,
  11. bar: 'static value'
  12. })

::: warning data 替换 状态机上定义的默认context; 它没有合并。 此行为将在下一个主要版本中更改。 :::

完成数据

当子状态机到达其顶级最终状态时,它可以在“done”事件中发送数据(例如,{ type: 'done.invoke.someId', data: .. .})。 这个“完成的数据”是在最终状态的data属性上指定的:

  1. const secretMachine = createMachine({
  2. id: 'secret',
  3. initial: 'wait',
  4. context: {
  5. secret: '42'
  6. },
  7. states: {
  8. wait: {
  9. after: {
  10. 1000: { target: 'reveal' }
  11. }
  12. },
  13. reveal: {
  14. type: 'final',
  15. data: {
  16. secret: (context, event) => context.secret
  17. }
  18. }
  19. }
  20. });
  21. const parentMachine = createMachine({
  22. id: 'parent',
  23. initial: 'pending',
  24. context: {
  25. revealedSecret: undefined
  26. },
  27. states: {
  28. pending: {
  29. invoke: {
  30. id: 'secret',
  31. src: secretMachine,
  32. onDone: {
  33. target: 'success',
  34. actions: assign({
  35. revealedSecret: (context, event) => {
  36. // event is:
  37. // { type: 'done.invoke.secret', data: { secret: '42' } }
  38. return event.data.secret;
  39. }
  40. })
  41. }
  42. }
  43. },
  44. success: {
  45. type: 'final'
  46. }
  47. }
  48. });
  49. const service = interpret(parentMachine)
  50. .onTransition((state) => console.log(state.context))
  51. .start();
  52. // => { revealedSecret: undefined }
  53. // ...
  54. // => { revealedSecret: '42' }

发送事件

  • 要从 状态机发送到 状态机,请使用 sendParent(event)(采用与 send(...) 相同的参数)
  • 要从 状态机发送到 状态机,请使用 send(event, { to: <child ID> })

::: warning send(...)sendParent(...) 动作 创建者 不是 命令式向状态机发送事件。 它们是返回动作对象的纯函数 描述要发送的内容,例如,{ type: 'xstate.send', event: ... }解释(interpret) 将读取这些对象,然后发送它们。

阅读有关send的更多信息 :::

下面是两台状态机 pingMachinepongMachine 相互通信的例子:

  1. import { createMachine, interpret, send, sendParent } from 'xstate';
  2. // 父状态机
  3. const pingMachine = createMachine({
  4. id: 'ping',
  5. initial: 'active',
  6. states: {
  7. active: {
  8. invoke: {
  9. id: 'pong',
  10. src: pongMachine
  11. },
  12. // 将“PING”事件发送到 ID 为“pong”的子状态机
  13. entry: send({ type: 'PING' }, { to: 'pong' }),
  14. on: {
  15. PONG: {
  16. actions: send({ type: 'PING' }, { to: 'pong', delay: 1000 })
  17. }
  18. }
  19. }
  20. }
  21. });
  22. // 调用子状态机
  23. const pongMachine = createMachine({
  24. id: 'pong',
  25. initial: 'active',
  26. states: {
  27. active: {
  28. on: {
  29. PING: {
  30. // 向父状态机发送“PONG”事件
  31. actions: sendParent('PONG', {
  32. delay: 1000
  33. })
  34. }
  35. }
  36. }
  37. }
  38. });
  39. const service = interpret(pingMachine).start();
  40. // => 'ping'
  41. // ...
  42. // => 'pong'
  43. // ..
  44. // => 'ping'
  45. // ...
  46. // => 'pong'
  47. // ...

发送响应

被调用的服务(或 创建的 演员)可以 响应 另一个 服务/演员; 即,它可以发送事件 响应 另一个 服务/演员 发送的事件。 这是通过 respond(...) 动作 创建者完成的。

例如,下面的 'client' 状态机将 'CODE' 事件发送到调用的 'auth-server' 服务,然后在 1 秒后响应 'TOKEN' 事件。

  1. import { createMachine, send, actions } from 'xstate';
  2. const { respond } = actions;
  3. const authServerMachine = createMachine({
  4. id: 'server',
  5. initial: 'waitingForCode',
  6. states: {
  7. waitingForCode: {
  8. on: {
  9. CODE: {
  10. actions: respond('TOKEN', { delay: 1000 })
  11. }
  12. }
  13. }
  14. }
  15. });
  16. const authClientMachine = createMachine({
  17. id: 'client',
  18. initial: 'idle',
  19. states: {
  20. idle: {
  21. on: {
  22. AUTH: { target: 'authorizing' }
  23. }
  24. },
  25. authorizing: {
  26. invoke: {
  27. id: 'auth-server',
  28. src: authServerMachine
  29. },
  30. entry: send({ type: 'CODE' }, { to: 'auth-server' }),
  31. on: {
  32. TOKEN: { target: 'authorized' }
  33. }
  34. },
  35. authorized: {
  36. type: 'final'
  37. }
  38. }
  39. });

这个特定的例子可以使用 sendParent(...) 来达到同样的效果; 不同之处在于 respond(...) 会将一个事件发送回接收到的事件的来源,它可能不一定是父状态机。

多服务

你可以通过在数组中指定每个服务来调用多个服务:

  1. // ...
  2. invoke: [
  3. { id: 'service1', src: 'someService' },
  4. { id: 'service2', src: 'someService' },
  5. { id: 'logService', src: 'logService' }
  6. ],
  7. // ...

每次调用都会创建该服务的 实例,因此即使多个服务的 src 相同(例如,上面的 'someService'),也会调用多个 'someService' 的实例。

配置服务

调用源(服务)的配置方式类似于东走、守卫等的配置方式,通过将 src 指定为字符串并在 Machine 选项的 services 属性中定义它们:

  1. const fetchUser = // (和上面例子相同)
  2. const userMachine = createMachine(
  3. {
  4. id: 'user',
  5. // ...
  6. states: {
  7. // ...
  8. loading: {
  9. invoke: {
  10. src: 'getUser',
  11. // ...
  12. }
  13. },
  14. // ...
  15. }
  16. },
  17. {
  18. services: {
  19. getUser: (context, event) => fetchUser(context.user.id)
  20. }
  21. );

调用 src 也可以指定为一个对象 ,它用它的 type 和其他相关的元数据来描述调用源。 这可以从 meta.src 参数中的 services 选项中读取:

  1. const machine = createMachine(
  2. {
  3. initial: 'searching',
  4. states: {
  5. searching: {
  6. invoke: {
  7. src: {
  8. type: 'search',
  9. endpoint: 'example.com'
  10. }
  11. // ...
  12. }
  13. // ...
  14. }
  15. }
  16. },
  17. {
  18. services: {
  19. search: (context, event, { src }) => {
  20. console.log(src);
  21. // => { endpoint: 'example.com' }
  22. }
  23. }
  24. }
  25. );

测试

通过将服务指定为上面的字符串,可以通过使用 .withConfig() 指定替代实现来完成“模拟”服务:

  1. import { interpret } from 'xstate';
  2. import { assert } from 'chai';
  3. import { userMachine } from '../path/to/userMachine';
  4. const mockFetchUser = async (userId) => {
  5. // 模拟任何你想要的,但确保使用相同的行为和响应格式
  6. return { name: 'Test', location: 'Anywhere' };
  7. };
  8. const testUserMachine = userMachine.withConfig({
  9. services: {
  10. getUser: (context, event) => mockFetchUser(context.id)
  11. }
  12. });
  13. describe('userMachine', () => {
  14. it('should go to the "success" state when a user is found', (done) => {
  15. interpret(testUserMachine)
  16. .onTransition((state) => {
  17. if (state.matches('success')) {
  18. assert.deepEqual(state.context.user, {
  19. name: 'Test',
  20. location: 'Anywhere'
  21. });
  22. done();
  23. }
  24. })
  25. .start();
  26. });
  27. });

引用服务

服务(和 演员,它们是衍生的服务)可以从 .children 属性直接在 状态对象 上引用。 state.children 对象是服务 ID(键)到这些服务实例(值)的映射:

  1. const machine = createMachine({
  2. // ...
  3. invoke: [
  4. { id: 'notifier', src: createNotifier },
  5. { id: 'logger', src: createLogger }
  6. ]
  7. // ...
  8. });
  9. const service = interpret(machine)
  10. .onTransition((state) => {
  11. state.children.notifier; // 来自 createNotifier() 的服务
  12. state.children.logger; // 来自 createLogger() 的服务
  13. })
  14. .start();

当 JSON 序列化时,state.children 对象是服务 ID(键)到包含有关该服务的元数据的对象的映射。

快速参考

invoke 属性

  1. const machine = createMachine({
  2. // ...
  3. states: {
  4. someState: {
  5. invoke: {
  6. // `src` 属性可以是:
  7. // - a string
  8. // - a machine
  9. // - a function that returns...
  10. src: (context, event) => {
  11. // - a promise
  12. // - a callback handler
  13. // - an observable
  14. },
  15. id: 'some-id',
  16. //(可选)将状态机事件转发到被调用的服务(目前仅适用于状态机!)
  17. autoForward: true,
  18. //(可选)调用的promise/observable/machine完成时的转换
  19. onDone: { target: /* ... */ },
  20. // (可选)当被调用的服务发生错误时的转换
  21. onError: { target: /* ... */ }
  22. }
  23. }
  24. }
  25. });

调用 Promises

  1. // 返回 Promise 的函数
  2. const getDataFromAPI = () => fetch(/* ... */)
  3. .then(data => data.json());
  4. // ...
  5. {
  6. invoke: (context, event) => getDataFromAPI,
  7. // resolved promise
  8. onDone: {
  9. target: 'success',
  10. // resolve promise 数据位于 event.data 属性上
  11. actions: (context, event) => console.log(event.data)
  12. },
  13. // rejected promise
  14. onError: {
  15. target: 'failure',
  16. // rejected promise 数据位于 event.data 属性上
  17. actions: (context, event) => console.log(event.data)
  18. }
  19. }
  20. // ...

调用 Callbacks

  1. // ...
  2. {
  3. invoke: (context, event) => (callback, onReceive) => {
  4. // 将事件发送回父级
  5. callback({ type: 'SOME_EVENT' });
  6. // 接收来自父级的事件
  7. onReceive(event => {
  8. if (event.type === 'DO_SOMETHING') {
  9. // ...
  10. }
  11. });
  12. },
  13. // callback 错误
  14. onError: {
  15. target: 'failure',
  16. // 错误数据位于 event.data 属性上
  17. actions: (context, event) => console.log(event.data)
  18. }
  19. },
  20. on: {
  21. SOME_EVENT: { /* ... */ }
  22. }

调用 Observables

  1. import { map } from 'rxjs/operators';
  2. // ...
  3. {
  4. invoke: {
  5. src: (context, event) => createSomeObservable(/* ... */).pipe(
  6. map(value => ({ type: 'SOME_EVENT', value }))
  7. ),
  8. onDone: 'finished'
  9. }
  10. },
  11. on: {
  12. SOME_EVENT: /* ... */
  13. }
  14. // ...

调用 状态机

  1. const someMachine = createMachine({ /* ... */ });
  2. // ...
  3. {
  4. invoke: {
  5. src: someMachine,
  6. onDone: {
  7. target: 'finished',
  8. actions: (context, event) => {
  9. // 子状态机的完成数据(其最终状态的 .data 属性)
  10. console.log(event.data);
  11. }
  12. }
  13. }
  14. }
  15. // ...