上下文 Context

:rocket: 快速参考

虽然 有限 状态在有限状态机和状态图中是明确定义的,但表示 定量数据(例如,任意字符串、数字、对象等)可能是无限的状态被表示为 扩展状态。 这使得状态图对于现实生活中的应用程序更有用。

在 XState 中,扩展状态被称为 上下文(context)。 下面是如何使用context来模拟填充一杯水的示例:

  1. import { createMachine, assign } from 'xstate';
  2. // 增加上下文量的动作
  3. const addWater = assign({
  4. amount: (context, event) => context.amount + 1
  5. });
  6. // 警卫检查玻璃是否已满
  7. function glassIsFull(context, event) {
  8. return context.amount >= 10;
  9. }
  10. const glassMachine = createMachine(
  11. {
  12. id: 'glass',
  13. // 状态图的初始上下文(扩展状态)
  14. context: {
  15. amount: 0
  16. },
  17. initial: 'empty',
  18. states: {
  19. empty: {
  20. on: {
  21. FILL: {
  22. target: 'filling',
  23. actions: 'addWater'
  24. }
  25. }
  26. },
  27. filling: {
  28. // 瞬态过渡
  29. always: {
  30. target: 'full',
  31. cond: 'glassIsFull'
  32. },
  33. on: {
  34. FILL: {
  35. target: 'filling',
  36. actions: 'addWater'
  37. }
  38. }
  39. },
  40. full: {}
  41. }
  42. },
  43. {
  44. actions: { addWater },
  45. guards: { glassIsFull }
  46. }
  47. );

当前上下文在 State 上被引用为 state.context

  1. const nextState = glassMachine.transition(glassMachine.initialState, {
  2. type: 'FILL'
  3. });
  4. nextState.context;
  5. // => { amount: 1 }

初始化 Context

初始上下文在 Machinecontext 属性上指定:

  1. const counterMachine = createMachine({
  2. id: 'counter',
  3. // 初始 context
  4. context: {
  5. count: 0,
  6. message: 'Currently empty',
  7. user: {
  8. name: 'David'
  9. },
  10. allowedToIncrement: true
  11. // ... 等等。
  12. },
  13. states: {
  14. // ...
  15. }
  16. });

对于动态context(即初始值是从外部检索或提供的context),你可以使用状态机工厂函数,使用提供的上下文值创建状态机(实现可能会有所不同):

  1. const createCounterMachine = (count, time) => {
  2. return createMachine({
  3. id: 'counter',
  4. // 从函数参数提供的值
  5. context: {
  6. count,
  7. time
  8. }
  9. // ...
  10. });
  11. };
  12. const counterMachine = createCounterMachine(42, Date.now());

或者对于现有状态机,应该使用machine.withContext(...)

  1. const counterMachine = createMachine({
  2. /* ... */
  3. });
  4. // 动态检索
  5. const someContext = { count: 42, time: Date.now() };
  6. const dynamicCounterMachine = counterMachine.withContext(someContext);

可以从状态机的初始状态,检索状态机的初始上下文:

  1. dynamicCounterMachine.initialState.context;
  2. // => { count: 42, time: 1543687816981 }

这比直接访问 machine.context 更可取,因为初始状态是通过初始 assign(...) 操作和瞬态转换(如果有)计算的。

分配(assign)动作

assign() 操作用于更新状态机的 context。 它采用上下文“分配器”,它表示应如何分配当前上下文中的值。

参数 类型 描述
assigner object or function 将值分配给 context 的对象分配器或函数分配器(见下文)

“assigner” 可以是一个对象(推荐):

  1. import { createMachine, assign } from 'xstate';
  2. // 示例:属性分配器 assigner
  3. // ...
  4. actions: assign({
  5. // 通过事件值增加当前计数
  6. count: (context, event) => context.count + event.value,
  7. // 为消息分配静态值(不需要函数)
  8. message: 'Count changed'
  9. }),
  10. // ...

或者它可以是一个返回更新状态的函数:

  1. // 示例:上下文 assigner
  2. // ...
  3. // 返回部分(或全部)更新的上下文
  4. actions: assign((context, event) => {
  5. return {
  6. count: context.count + event.value,
  7. message: 'Count changed'
  8. }
  9. }),
  10. // ...

上面的属性分配器和上下文分配器函数签名都给出了 3 个参数:contexteventmeta

参数 类型 描述
context TContext 状态机的当前上下文(扩展状态)
event EventObject 触发assign动作的事件
meta AssignMeta 带有元数据的对象(见下文)

meta 对象包含:

  • state - 正常转换中的当前状态(初始状态转换为 undefined
  • action - 分配动作

::: warning assign(...) 函数是一个动作创建者; 它是一个纯函数,它只返回一个动作对象并且 命令式地对上下文进行赋值。 :::

动作顺序

自定义动作,始终指向转换中的 下一个状态 执行。 当状态转换具有assign(...)动作时,这些动作总是被批处理和计算 首个 执行,以确定下一个状态。 这是因为状态是有限状态和扩展状态(上下文)的组合。

例如,在此计数器状态机中,自定义操作将无法按预期工作:

  1. const counterMachine = createMachine({
  2. id: 'counter',
  3. context: { count: 0 },
  4. initial: 'active',
  5. states: {
  6. active: {
  7. on: {
  8. INC_TWICE: {
  9. actions: [
  10. (context) => console.log(`Before: ${context.count}`),
  11. assign({ count: (context) => context.count + 1 }), // count === 1
  12. assign({ count: (context) => context.count + 1 }), // count === 2
  13. (context) => console.log(`After: ${context.count}`)
  14. ]
  15. }
  16. }
  17. }
  18. }
  19. });
  20. interpret(counterMachine).start().send({ type: 'INC_TWICE' });
  21. // => "Before: 2"
  22. // => "After: 2"

这是因为两个 assign(...) 动作总是是按顺序批处理并首先执行(在微任务中),所以下一个状态 context{ count: 2 },它被传递给两个自定义操作。 另一种思考这种转变的方式是阅读它:

当处于 active 状态并且发生 INC_TWICE 事件时,下一个状态是更新了 context.countactive 状态, 然后 在该状态上执行这些自定义操作。

重构它以获得所需结果的一个好方法是使用显式 上一个 值对 context 进行建模,如果需要的话:

  1. const counterMachine = createMachine({
  2. id: 'counter',
  3. context: { count: 0, prevCount: undefined },
  4. initial: 'active',
  5. states: {
  6. active: {
  7. on: {
  8. INC_TWICE: {
  9. actions: [
  10. (context) => console.log(`Before: ${context.prevCount}`),
  11. assign({
  12. count: (context) => context.count + 1,
  13. prevCount: (context) => context.count
  14. }), // count === 1, prevCount === 0
  15. assign({ count: (context) => context.count + 1 }), // count === 2
  16. (context) => console.log(`After: ${context.count}`)
  17. ]
  18. }
  19. }
  20. }
  21. }
  22. });
  23. interpret(counterMachine).start().send({ type: 'INC_TWICE' });
  24. // => "Before: 0"
  25. // => "After: 2"

这样做的好处是:

  1. 扩展状态(上下文)被更明确地建模
  2. 没有隐含的中间状态,防止难以捕捉的错误
  3. 动作顺序更加独立(“Before”日志甚至可以在“After”日志之后!)
  4. 促进测试和检查状态

注意

  • 🚫 永远不要在外部改变状态机的“上下文”。 任何事情的发生都是有原因的,并且每个上下文更改都应该由于事件而明确发生。
  • 更喜欢assign({ ... }) 的对象语法。 这使得未来的分析工具可以预测属性是 如何 改变的。
  • 动作可以堆叠,并按顺序运行:
  1. // ...
  2. actions: [
  3. assign({ count: 3 }), // context.count === 3
  4. assign({ count: context => context.count * 2 }) // context.count === 6
  5. ],
  6. // ...
  • 就像 actions 一样,最好将 assign() 操作表示为字符串或函数,然后在状态机选项中引用它们:

```js {5} const countMachine = createMachine({ initial: ‘start’, context: { count: 0 } states: { start: { entry: ‘increment’ } } }, { actions: { increment: assign({ count: context => context.count + 1 }), decrement: assign({ count: context => context.count - 1 }) } });

  1. 或者作为命名函数(与上面相同的结果):
  2. ```js {9}
  3. const increment = assign({ count: context => context.count + 1 });
  4. const decrement = assign({ count: context => context.count - 1 });
  5. const countMachine = createMachine({
  6. initial: 'start',
  7. context: { count: 0 }
  8. states: {
  9. start: {
  10. // 命名函数
  11. entry: increment
  12. }
  13. }
  14. });
  • 理想情况下,context 应该可以表示为一个普通的 JavaScript 对象; 即,它应该可以序列化为 JSON。
  • 由于引发了 assign() 动作,所以在执行其他动作之前更新上下文。 这意味着同一步骤中的其他操作将获得 更新的 context,而不是执行 assign() 操作之前的内容。 你不应该依赖状态的行动顺序,但请记住这一点。 有关更多详细信息,请参阅 操作顺序

TypeScript

为了正确的类型推断,将上下文类型作为第一个类型参数添加到 createMachine<TContext, ...>

  1. interface CounterContext {
  2. count: number;
  3. user?: {
  4. name: string;
  5. };
  6. }
  7. const machine = createMachine<CounterContext>({
  8. // ...
  9. context: {
  10. count: 0,
  11. user: undefined
  12. }
  13. // ...
  14. });

如果适用,你还可以使用 typeof ... 作为速记:

  1. const context = {
  2. count: 0,
  3. user: { name: '' }
  4. };
  5. const machine = createMachine<typeof context>({
  6. // ...
  7. context
  8. // ...
  9. });

在大多数情况下,assign(...) 动作中contextevent 的类型将根据传递给createMachine<TContext, TEvent> 的类型参数自动推断:

  1. interface CounterContext {
  2. count: number;
  3. }
  4. const machine = createMachine<CounterContext>({
  5. // ...
  6. context: {
  7. count: 0
  8. },
  9. // ...
  10. {
  11. on: {
  12. INCREMENT: {
  13. // 大多数情况下自动推断
  14. actions: assign({
  15. count: (context) => {
  16. // context: { count: number }
  17. return context.count + 1;
  18. }
  19. })
  20. }
  21. }
  22. }
  23. });

然而,TypeScript 的推断并不完美,所以负责任的做法是将上下文和事件作为泛型添加到 assign<Context, Event>(...) 中:

```ts {3} // … on: { INCREMENT: { // 泛型保证正确的推理 actions: assign({ count: (context) => { // context: { count: number } return context.count + 1; } }); } } // …

  1. ## 快速参考
  2. **设置初始上下文**
  3. ```js
  4. const machine = createMachine({
  5. // ...
  6. context: {
  7. count: 0,
  8. user: undefined
  9. // ...
  10. }
  11. });

设置动态初始上下文

  1. const createSomeMachine = (count, user) => {
  2. return createMachine({
  3. // ...
  4. // 从参数提供; 你的实施可能会有所不同
  5. context: {
  6. count,
  7. user
  8. // ...
  9. }
  10. });
  11. };

设置自定义初始上下文

  1. const machine = createMachine({
  2. // ...
  3. // 从参数提供; 你的实施可能会有所不同
  4. context: {
  5. count: 0,
  6. user: undefined
  7. // ...
  8. }
  9. });
  10. const myMachine = machine.withContext({
  11. count: 10,
  12. user: {
  13. name: 'David'
  14. }
  15. });

分配给上下文

  1. const machine = createMachine({
  2. // ...
  3. context: {
  4. count: 0,
  5. user: undefined
  6. // ...
  7. },
  8. // ...
  9. on: {
  10. INCREMENT: {
  11. actions: assign({
  12. count: (context, event) => context.count + 1
  13. })
  14. }
  15. }
  16. });

分配(静态)

  1. // ...
  2. actions: assign({
  3. counter: 42
  4. }),
  5. // ...

分配(属性)

  1. // ...
  2. actions: assign({
  3. counter: (context, event) => {
  4. return context.count + event.value;
  5. }
  6. }),
  7. // ...

分配 (上下文)

  1. // ...
  2. actions: assign((context, event) => {
  3. return {
  4. counter: context.count + event.value,
  5. time: event.time,
  6. // ...
  7. }
  8. }),
  9. // ...

分配 (多个)

  1. // ...
  2. // 假设 context.count === 1
  3. actions: [
  4. // 将 context.count 分配给 1 + 1 = 2
  5. assign({ count: (context) => context.count + 1 }),
  6. // 将 context.count 分配给 2 * 3 = 6
  7. assign({ count: (context) => context.count * 3 })
  8. ],
  9. // ...