# 上下文

¥Context

🚀快速参考

¥🚀 Quick Reference

这些 XState v4 文档不再维护

XState v5 现已推出!阅读有关 XState v5 的更多信息 (opens new window)

¥XState v5 is out now! Read more about XState v5 (opens new window)

🆕 在我们的新文档中查找有关 XState 中的上下文 (opens new window) 的更多信息。

¥🆕 Find more about context in XState (opens new window) in our new docs.

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

¥While finite states are well-defined in finite state machines and statecharts, state that represents quantitative data (e.g., arbitrary strings, numbers, objects, etc.) that can be potentially infinite is represented as extended state (opens new window) instead. This makes statecharts much more useful for real-life applications.

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

¥In XState, extended state is known as context. Below is an example of how context is used to simulate filling a glass of water:

import { createMachine, assign } from 'xstate';

// Action to increment the context amount
const addWater = assign({
  amount: (context, event) => context.amount + 1
});

// Guard to check if the glass is full
function glassIsFull(context, event) {
  return context.amount >= 10;
}

const glassMachine = createMachine(
  {
    id: 'glass',
    // the initial context (extended state) of the statechart
    context: {
      amount: 0
    },
    initial: 'empty',
    states: {
      empty: {
        on: {
          FILL: {
            target: 'filling',
            actions: 'addWater'
          }
        }
      },
      filling: {
        // Transient transition
        always: {
          target: 'full',
          cond: 'glassIsFull'
        },
        on: {
          FILL: {
            target: 'filling',
            actions: 'addWater'
          }
        }
      },
      full: {}
    }
  },
  {
    actions: { addWater },
    guards: { glassIsFull }
  }
);

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

¥The current context is referenced on the State as state.context:

const nextState = glassMachine.transition(glassMachine.initialState, {
  type: 'FILL'
});

nextState.context;
// => { amount: 1 }

# 初始上下文

¥Initial Context

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

¥The initial context is specified on the context property of the Machine:

const counterMachine = createMachine({
  id: 'counter',
  // initial context
  context: {
    count: 0,
    message: 'Currently empty',
    user: {
      name: 'David'
    },
    allowedToIncrement: true
    // ... etc.
  },
  states: {
    // ...
  }
});

机器的 context 属性也可以延迟初始化;即,在实际创建/使用机器之前不会创建上下文:

¥The context property of the machine can also be initialized lazily; i.e., the context will not be created until the machine is actually created/used:

const counterMachine = createMachine({
  id: 'counter',
  // initial context
  context: () => ({
    count: 0,
    message: 'Currently empty',
    user: {
      name: 'David'
    },
    allowedToIncrement: true
    // ... etc.
  }),
  states: {
    // ...
  }
});

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

¥For dynamic context (that is, context whose initial value is retrieved or provided externally), you can use a machine factory function that creates the machine with the provided context values (implementation may vary):

const createCounterMachine = (count, time) => {
  return createMachine({
    id: 'counter',
    // values provided from function arguments
    context: {
      count,
      time
    }
    // ...
  });
};

const counterMachine = createCounterMachine(42, Date.now());

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

¥Or for existing machines, machine.withContext(...) should be used:

const counterMachine = createMachine({
  /* ... */
});

// retrieved dynamically
const someContext = { count: 42, time: Date.now() };

const dynamicCounterMachine = counterMachine.withContext(someContext);

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

¥The initial context of a machine can be retrieved from its initial state:

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

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

¥This is preferred to accessing machine.context directly, since the initial state is computed with initial assign(...) actions and transient transitions, if any.

# 指定操作

¥Assign Action

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

¥The assign() action is used to update the machine's context. It takes the context "assigner", which represents how values in the current context should be assigned.

争论 类型 描述
assigner 对象或函数 context 赋值的对象分配器或函数分配器(见下文)

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

¥The "assigner" can be an object (recommended):

import { createMachine, assign } from 'xstate';
// example: property assigner

// ...
  actions: assign({
    // increment the current count by the event value
    count: (context, event) => context.count + event.value,

    // assign static value to the message (no function needed)
    message: 'Count changed'
  }),
// ...

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

¥Or it can be a function that returns the updated state:

// example: context assigner

// ...

  // return a partial (or full) updated context
  actions: assign((context, event) => {
    return {
      count: context.count + event.value,
      message: 'Count changed'
    }
  }),
// ...

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

¥Both the property assigner and context assigner function signatures above are given 3 arguments: the context, event, and meta:

争论 类型 描述
context TContext 机器的当前上下文(扩展状态)
event EventObject 触发 assign 操作的事件
meta <徽章文本="4.7+" /> AssignMeta 具有元数据的对象(见下文)

meta 对象包含:

¥The meta object contains:

  • state - 正常转换中的当前状态(undefined 表示初始状态转换)

    ¥state - the current state in a normal transition (undefined for the initial state transition)

  • action - 分配动作

    ¥action - the assign action

警告

转让人必须是纯粹的;它们不应包含任何副作用。这是因为 assign(...) 操作涉及确定下一个状态,并且分配器中的副作用可能会引入不确定性。

¥Assigners must be pure; they should not contain any side-effects. This is because the assign(...) action is involved in determining the next state, and side-effects in the assigner could introduce nondeterminism.

actions: assign({
  count: (context) => {
    doSomeSideEffect(); // ❌ No side-effects in assignment functions

    return context.count + 1;
  }
});

警告

assign(...) 功能是动作创建者;它是一个纯函数,仅返回一个操作对象,并不强制对上下文进行赋值。

¥The assign(...) function is an action creator; it is a pure function that only returns an action object and does not imperatively make assignments to the context.

# 行动顺序

¥Action Order

警告

在 XState 版本 5 中,此行为将发生变化,并且 assign(...) 操作将按顺序调用,而不是按优先级排列,根据 SCXML,这是不正确的行为。

¥In XState version 5, this behavior will change and assign(...) actions will be called in order instead of being prioritized, which is incorrect behavior according to SCXML.

要在版本 4 中获得此行为,请将 preserveActionOrder: true 添加到计算机配置中:

¥To get this behavior in version 4, add preserveActionOrder: true to the machine config:

const counterMachine = createMachine({
  preserveActionOrder: true, // Ensures that assign actions are called in order
  // ...
  context: { count: 0 },
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.count}`), // "Before: 0"
            assign({ count: (context) => context.count + 1 }), // count === 1
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`) // "After: 2"
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 0"
// => "After: 2"

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

¥Custom actions are always executed with regard to the next state in the transition. When a state transition has assign(...) actions, those actions are always batched and computed first, to determine the next state. This is because a state is a combination of the finite state and the extended state (context).

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

¥For example, in this counter machine, the custom actions will not work as expected:

const counterMachine = createMachine({
  id: 'counter',
  context: { count: 0 },
  initial: 'active',
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.count}`), // "Before: 2"
            assign({ count: (context) => context.count + 1 }), // count === 1
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`) // "After: 2"
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 2"
// => "After: 2"

这是因为两个 assign(...) 操作都是按顺序批处理并首先执行(以微步),因此下一个状态 context{ count: 2 },它会传递给两个自定义操作。思考这一转变的另一种方式是这样解读:

¥This is because both assign(...) actions are batched in order and executed first (in the microstep), so the next state context is { count: 2 }, which is passed to both custom actions. Another way of thinking about this transition is reading it like:

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

¥When in the active state and the INC_TWICE event occurs, the next state is the active state with context.count updated, and then these custom actions are executed on that state.

重构它以获得所需结果的一个好方法是使用明确的先前值对 context 进行建模(如果需要的话):

¥A good way to refactor this to get the desired result is modeling the context with explicit previous values, if those are needed:

const counterMachine = createMachine({
  id: 'counter',
  context: { count: 0, prevCount: undefined },
  initial: 'active',
  states: {
    active: {
      on: {
        INC_TWICE: {
          actions: [
            (context) => console.log(`Before: ${context.prevCount}`),
            assign({
              count: (context) => context.count + 1,
              prevCount: (context) => context.count
            }), // count === 1, prevCount === 0
            assign({ count: (context) => context.count + 1 }), // count === 2
            (context) => console.log(`After: ${context.count}`)
          ]
        }
      }
    }
  }
});

interpret(counterMachine).start().send({ type: 'INC_TWICE' });
// => "Before: 0"
// => "After: 2"

这样做的好处是:

¥The benefits of this are:

  1. 扩展状态(上下文)的建模更加明确

    ¥The extended state (context) is modeled more explicitly

  2. 没有隐式的中间状态,防止难以捕获的错误

    ¥There are no implicit intermediate states, preventing hard-to-catch bugs

  3. 操作顺序更加独立("前" 日志甚至可以在 "后" 日志之后!)

    ¥The action order is more independent (the "Before" log can even go after the "After" log!)

  4. 促进测试和检查状态

    ¥Facilitates testing and examining the state

# 注意

¥Notes

  • 🚫 切勿从外部改变机器的 context。一切发生都有原因,并且每个上下文更改都应该由于事件而明确发生。

    ¥🚫 Never mutate the machine's context externally. Everything happens for a reason, and every context change should happen explicitly due to an event.

  • 更喜欢 assign({ ... }) 的对象语法。这使得未来的分析工具可以预测某些属性如何以声明方式发生变化。

    ¥Prefer the object syntax of assign({ ... }). This makes it possible for future analysis tools to predict how certain properties can change declaratively.

  • 作业可以堆叠,并按顺序运行:

    ¥Assignments can be stacked, and will run sequentially:

// ...
  actions: [
    assign({ count: 3 }), // context.count === 3
    assign({ count: context => context.count * 2 }) // context.count === 6
  ],
// ...
  • 就像 actions 一样,最好将 assign() 操作表示为字符串或函数,然后在机器选项中引用它们:

    ¥Just like with actions, it's best to represent assign() actions as strings or functions, and then reference them in the machine options:





 










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 })
  }
});

或者作为命名函数(与上面的结果相同):

¥Or as named functions (same result as above):









 





const increment = assign({ count: context => context.count + 1 });
const decrement = assign({ count: context => context.count - 1 });

const countMachine = createMachine({
  initial: 'start',
  context: { count: 0 }
  states: {
    start: {
      // Named function
      entry: increment
    }
  }
});
  • 理想情况下,context 应该可以表示为纯 JavaScript 对象;即,它应该可序列化为 JSON。

    ¥Ideally, the context should be representable as a plain JavaScript object; i.e., it should be serializable as JSON.

  • 由于引发了 assign() 操作,因此在执行其他操作之前会更新上下文。这意味着同一步骤中的其他操作将获取更新后的 context,而不是执行 assign() 操作之前的值。你不应该依赖你状态的行动指令,但请记住这一点。详细信息请参见 行动顺序

    ¥Since assign() actions are raised, the context is updated before other actions are executed. This means that other actions within the same step will get the updated context rather than what it was before the assign() action was executed. You shouldn't rely on action order for your states, but keep this in mind. See action order for more details.

# TypeScript

为了正确的类型推断,请将上下文类型添加到机器的 schema 属性中:

¥For proper type inference, add the context type to the schema property of the machine:

import { createMachine } from 'xstate';

interface CounterContext {
  count: number;
  user?: {
    name: string;
  };
}

const machine = createMachine({
  schema: {
    context: {} as CounterContext
  },
  // ...
  context: {
    count: 0,
    user: undefined
  }
  // ...
});

如果适用,你还可以使用 typeof ... 作为简写:

¥When applicable, you can also use typeof ... as a shorthand:

const context = {
  count: 0,
  user: { name: '' }
};

const machine = createMachine({
  schema: {
    context: {} as typeof context
  },
  // ...
  context
  // ...
});

在大多数情况下,assign(...) 操作中 contextevent 的类型将从传递到 schema 的类型参数中自动推断出来:

¥In most cases, the types for context and event in assign(...) actions will be automatically inferred from the type parameters passed into schema:

interface CounterContext {
  count: number;
}

const machine = createMachine({
  schema: {
    context: {} as CounterContext
  },
  // ...
  context: {
    count: 0
  },
  // ...
  {
    on: {
      INCREMENT: {
        // Inferred automatically in most cases
        actions: assign({
          count: (context) => {
            // context: { count: number }
            return context.count + 1;
          }
        })
      }
    }
  }
});

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

¥However, TypeScript inference isn't perfect, so the responsible thing to do is to add the context and event as generics into assign<Context, Event>(...):



 











// ...
on: {
  INCREMENT: {
    // Generics guarantee proper inference
    actions: assign<CounterContext, CounterEvent>({
      count: (context) => {
        // context: { count: number }
        return context.count + 1;
      }
    });
  }
}
// ...

# 快速参考

¥Quick Reference

设置初始上下文

¥Set initial context

const machine = createMachine({
  // ...
  context: {
    count: 0,
    user: undefined
    // ...
  }
});

设置动态初始上下文

¥Set dynamic initial context

const createSomeMachine = (count, user) => {
  return createMachine({
    // ...
    // Provided from arguments; your implementation may vary
    context: {
      count,
      user
      // ...
    }
  });
};

设置自定义初始上下文

¥Set custom initial context

const machine = createMachine({
  // ...
  // Provided from arguments; your implementation may vary
  context: {
    count: 0,
    user: undefined
    // ...
  }
});

const myMachine = machine.withContext({
  count: 10,
  user: {
    name: 'David'
  }
});

分配给上下文

¥Assign to context

const machine = createMachine({
  // ...
  context: {
    count: 0,
    user: undefined
    // ...
  },
  // ...
  on: {
    INCREMENT: {
      actions: assign({
        count: (context, event) => context.count + 1
      })
    }
  }
});

分配(静态)

¥Assignment (static)

// ...
actions: assign({
  counter: 42
}),
// ...

转让(属性)

¥Assignment (property)

// ...
actions: assign({
  counter: (context, event) => {
    return context.count + event.value;
  }
}),
// ...

作业(上下文)

¥Assignment (context)

// ...
actions: assign((context, event) => {
  return {
    counter: context.count + event.value,
    time: event.time,
    // ...
  }
}),
// ...

作业(多项)

¥Assignment (multiple)

// ...
// assume context.count === 1
actions: [
  // assigns context.count to 1 + 1 = 2
  assign({ count: (context) => context.count + 1 }),
  // assigns context.count to 2 * 3 = 6
  assign({ count: (context) => context.count * 3 })
],
// ...