# 上下文
¥Context
这些 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
初始上下文在 Machine
的 context
属性上指定:
¥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 个参数:context
、event
和 meta
:
¥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 theINC_TWICE
event occurs, the next state is theactive
state withcontext.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:
扩展状态(上下文)的建模更加明确
¥The extended state (context) is modeled more explicitly
没有隐式的中间状态,防止难以捕获的错误
¥There are no implicit intermediate states, preventing hard-to-catch bugs
操作顺序更加独立("前" 日志甚至可以在 "后" 日志之后!)
¥The action order is more independent (the "Before" log can even go after the "After" log!)
促进测试和检查状态
¥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 representassign()
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 updatedcontext
rather than what it was before theassign()
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(...)
操作中 context
和 event
的类型将从传递到 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 })
],
// ...