转换 Transitions
转换定义了状态机如何对 事件 做出响应。 要了解更多信息,请参阅 状态图介绍 中的部分。
API
状态转换在状态节点的 on
属性中定义,:
```js {11,14-16} import { createMachine } from ‘xstate’;
const promiseMachine = createMachine({ id: ‘promise’, initial: ‘pending’, states: { pending: { on: { // 状态转换(简写) // 这相当于 { target: ‘resolved’ } RESOLVE: ‘resolved’,
// 状态转换 (object)
REJECT: {
target: 'rejected'
}
}
},
resolved: {
type: 'final'
},
rejected: {
type: 'final'
}
} });
const { initialState } = promiseMachine;
console.log(initialState.value); // => ‘pending’
const nextState = promiseMachine.transition(initialState, { type: ‘RESOLVE’ });
console.log(nextState.value); // => ‘resolved’
在上面的例子中,当状态机处于 `pending` 状态并且它接收到一个 `RESOLVE` 事件时,它会转换到 `resolved` 状态。
状态转换可以定义为:
- 一个字符串,例如 `RESOLVE: 'resolved'`
- 具有 `target` 属性的对象,例如 `RESOLVE: { target: 'resolved' }`,
- 转换对象数组,用于条件转换(请参阅 [守卫]($zh-guides-guards.md))
## 状态机 `.transition` 方法
如上所示, `machine.transition(...)` 方法是一个纯函数,它接受两个参数:
- `state` - 要转换的 [状态](./states.md)
- `event` - 导致转换的 [事件]($zh-guides-events.md)
它返回一个新的 [`State` 实例](./states.md#state-definition),这是采用当前状态和事件,启用的所有转换的结果。
```js {8}
const lightMachine = createMachine({
/* ... */
});
const greenState = lightMachine.initialState;
// 根据当前状态和事件确定下一个状态
const yellowState = lightMachine.transition(greenState, { type: 'TIMER' });
console.log(yellowState.value);
// => 'yellow'
选择启用转换
启用的转换 是将根据当前状态和事件有条件地进行的转换。 当且仅当:
在 分层状态机 中,转换的优先级取决于它们在树中的深度; 更深层次的转换更具体,因此具有更高的优先级。 这与 DOM 事件的工作方式类似:如果单击按钮,则直接在按钮上的单击事件处理程序比 window
上的单击事件处理程序更具体。
```js {10,21-22,27} const wizardMachine = createMachine({ id: ‘wizard’, initial: ‘open’, states: { open: { initial: ‘step1’, states: { step1: { on: { NEXT: { target: ‘step2’ } } }, step2: { / … / }, step3: { / … / } }, on: { NEXT: { target: ‘goodbye’ }, CLOSE: { target: ‘closed’ } } }, goodbye: { on: { CLOSE: { target: ‘closed’ } } }, closed: { type: ‘final’ } } });
// { open: ‘step1’ } const { initialState } = wizardMachine;
// ‘open.step1’ 上定义的 NEXT 转换取代了父’open’状态上定义的 NEXT 转换 const nextStepState = wizardMachine.transition(initialState, { type: ‘NEXT’ }); console.log(nextStepState.value); // => { open: ‘step2’ }
// ‘open.step1’ 上没有 CLOSE 转换,因此事件被传递到父 ‘open’ 状态,在那里它被定义 const closedState = wizardMachine.transition(initialState, { type: ‘CLOSE’ }); console.log(closedState.value); // => ‘closed’
## 事件描述符
事件描述符,是描述转换 将匹配的事件类型的字符串。 通常,这等效于发送到状态机的 `event` 对象上的 `event.type` 属性:
```js
// ...
{
on: {
// "CLICK"是事件描述符。
// 此转换匹配具有 { type: 'CLICK' } 的事件
CLICK: 'someState',
// "SUBMIT"是事件描述符。
// 此转换匹配具有 { type: 'SUBMIT' } 的事件
SUBMIT: 'anotherState'
}
}
// ...
其他事件描述符包括:
自转换
自转换是当一个状态转换到自身时,它 可以 退出然后重新进入自身。 自转换可以是 内部 或 外部 转换:
- 内部转换 不会退出也不会重新进入自身,但可能会进入不同的子状态。
- 外部转换 将退出并重新进入自身,也可能退出/进入子状态。
默认情况下,具有指定目标的所有转换都是外部的。
有关如何在自转换上执行进入/退出操作的更多详细信息,请参阅有关 自转换的操作。
内部转换
内部转换是不退出其状态节点的转换。 内部转换是通过指定 相对目标(例如,'.left'
)或通过在转换上显式设置 { internal: true }
来创建的。 例如,考虑一台状态机将一段文本设置为对齐 'left'
、 'right'
、 'center'
、或 'justify'
:
```js {14-17} import { createMachine } from ‘xstate’;
const wordMachine = createMachine({ id: ‘word’, initial: ‘left’, states: { left: {}, right: {}, center: {}, justify: {} }, on: { // 内部转换 LEFT_CLICK: ‘.left’, RIGHT_CLICK: { target: ‘.right’ }, // 同 ‘.right’ CENTER_CLICK: { target: ‘.center’, internal: true }, // 同 ‘.center’ JUSTIFY_CLICK: { target: ‘.justify’, internal: true } // 同 ‘.justify’ } });
上面的状态机将以 `'left'` 状态启动,并根据单击的内容在内部转换到其他子状态。 此外,由于转换是内部的,因此不会再次执行在父状态节点上定义的 `entry`, `exit` 或者任何其他的 `actions`。
具有 `{ target: undefined }` (或无 `target`)的转换也是内部转换:
```js {11-13}
const buttonMachine = createMachine({
id: 'button',
initial: 'inactive',
states: {
inactive: {
on: { PUSH: 'active' }
},
active: {
on: {
// 无 target - 内部转换
PUSH: {
actions: 'logPushed'
}
}
}
}
});
内部转换摘要:
EVENT: '.foo'
- 内部转换到子状态EVENT: { target: '.foo' }
- 内部转换到子状态(以'.'
开头)EVENT: undefined
- 禁止转换EVENT: { actions: [ ... ] }
- 内部自转换EVENT: { actions: [ ... ], internal: true }
- 内部自转换,同上EVENT: { target: undefined, actions: [ ... ] }
- 内部自转换,同上
外部转换
外部转换 将 退出并重新进入定义转换的状态节点。 在上面的例子中,父级 word
状态节点(根状态节点),将在其转换时执行 exit
和 entry
动作。
默认情况下,转换是外部的,但任何转换都可以通过在转换上显式设置 { internal: false }
来实现。
```js {4-7} // … on: { // 外部转换 LEFT_CLICK: ‘word.left’, RIGHT_CLICK: ‘word.right’, CENTER_CLICK: { target: ‘.center’, internal: false }, // 同 ‘word.center’ JUSTIFY_CLICK: { target: ‘word.justify’, internal: false } // 同 ‘word.justify’ } // …
上面的每个转换都是外部的,并且将执行父状态的 `exit` 和 `entry` 操作。
**外部转换摘要:**
- `EVENT: { target: 'foo' }` - 所有对兄弟状态的转换都是外部转换
- `EVENT: { target: '#someTarget' }` - 到其他节点的所有转换都是外部转换
- `EVENT: { target: 'same.foo' }` - 外部转换到自己的子级节点(相当于`{ target: '.foo', internal: false }`)
- `EVENT: { target: '.foo', internal: false }` - 外部转换到子节点
- 否则这将是一个内部转换
- `EVENT: { actions: [ ... ], internal: false }` - 外部自转换
- `EVENT: { target: undefined, actions: [ ... ], internal: false }` - 外部自转换,同上
## 瞬间转换
::: warning
空字符串语法 (`{ on: { '': ... } }`) 将在第 5 版中弃用。应该首选 4.11+ 版中新的 `always` 语法。请参阅下面关于 [无事件转换](#eventless-always-transitions) 的部分,它与瞬间转换相同。
:::
瞬间转换是由 [null 事件]($zh-guides-events.md#null-events) 触发的转换。 换句话说,只要满足任何条件,就会 _立即_ 进行转换(即,没有触发事件):
```js {14-17}
const gameMachine = createMachine(
{
id: 'game',
initial: 'playing',
context: {
points: 0
},
states: {
playing: {
on: {
// 瞬间转换 如果满足条件,将在(重新)进入 'playing' 状态后立即转换为 'win' 或 'lose'。
'': [
{ target: 'win', cond: 'didPlayerWin' },
{ target: 'lose', cond: 'didPlayerLose' }
],
// 自转换
AWARD_POINTS: {
actions: assign({
points: 100
})
}
}
},
win: { type: 'final' },
lose: { type: 'final' }
}
},
{
guards: {
didPlayerWin: (context, event) => {
// 检查玩家是否赢了
return context.points > 99;
},
didPlayerLose: (context, event) => {
// 检查玩家是否输了
return context.points < 0;
}
}
}
);
const gameService = interpret(gameMachine)
.onTransition((state) => console.log(state.value))
.start();
// 仍处于 'playing' 状态,因为不满足瞬间转换条件
// => 'playing'
// 当发送“AWARD_POINTS”时,会发生自我转换到“PLAYING”。
// 由于满足“didPlayerWin”条件,因此会进行到“win”的瞬间转换。
gameService.send('AWARD_POINTS');
// => 'win'
就像转换一样,可以将瞬间转换指定为单个转换(例如,'': 'someTarget'
)或条件转换数组。 如果没有满足瞬间转换的条件转换,则状态机保持相同状态。
对于每次内部或外部转换,始终 “sent” 空事件。
无事件 (“Always”) 转换
无事件转换,是当状态机处于定义的状态,并且其 cond
守卫为 true
时 始终进行 的转换。 他们被检查:
- 立即进入状态节点
- 每次状态机接收到一个可操作的事件(无论该事件是触发内部转换还是外部转换)
无事件转换在状态节点的 always
属性上定义:
```js {14-17} const gameMachine = createMachine( { id: ‘game’, initial: ‘playing’, context: { points: 0 }, states: { playing: { // 无事件转换 // 如果条件满足,将在进入 ‘playing’ 状态或接收到 AWARD_POINTS 事件后立即转换为 ‘win’ 或 ‘lose’。 always: [ { target: ‘win’, cond: ‘didPlayerWin’ }, { target: ‘lose’, cond: ‘didPlayerLose’ } ], on: { // 自转换 AWARD_POINTS: { actions: assign({ points: 100 }) } } }, win: { type: ‘final’ }, lose: { type: ‘final’ } } }, { guards: { didPlayerWin: (context, event) => { // 检测玩家是否赢了 return context.points > 99; }, didPlayerLose: (context, event) => { // 检测玩家是否输了 return context.points < 0; } } } );
const gameService = interpret(gameMachine) .onTransition((state) => console.log(state.value)) .start();
// 仍处于 ‘playing’ 状态,因为不满足瞬间转换条件 // => ‘playing’
// 当发送“AWARD_POINTS”时,会发生自我转换到“PLAYING”。 // 由于满足“didPlayerWin”条件,因此会进行到“win”的瞬间转换。 gameService.send({ type: ‘AWARD_POINTS’ }); // => ‘win’
### 无事件 vs. 通配符转换
- [通配符转换](#wildcard-descriptors) 在进入状态节点时不被检查。 无事件转换是,在做任何其他事情之前(甚至在进入动作的守卫判断之前)的转换。
- 无事件转换的重新判断,由任何可操作的事件触发。 通配符转换的重新判断,仅由与显式事件描述符不匹配的事件触发。
::: warning
如果误用无事件转换,则有可能创建无限循环。
无事件转换应该使用 `target`、`cond` + `target`、`cond` + `actions` 或 `cond` + `target` + `actions` 来定义。 目标(如果已声明)应与当前状态节点不同。 没有 `target` 和 `cond` 的无事件转换将导致无限循环。 如果 `cond` 守卫不断返回 `true`,则带有 `cond` 和 `actions` 的转换可能会陷入无限循环。
:::
::: tip
当检查无事件转换时,它们的守卫会被重复判断,直到它们都返回 false,或者验证了具有目标的转换。 在此过程中,每当某个守卫判断为 `true` 时,其关联的操作将被执行一次。 因此,在单个微任务期间,可能会多次执行一些没有目标的转换。这与普通转换形成对比,在普通转换中,最多只能进行一个转换。
:::
## 禁止转换
在 XState 中,“禁止”转换是一种指定不应随指定事件发生状态转换的转换。 也就是说,在禁止转换上不应发生任何事情,并且该事件不应由父状态节点处理。
通过将 `target` 明确指定为 `undefined` 来进行禁止转换。 这与将其指定为没有操作的内部转换相同:
```js {3}
on: {
// 禁止转换
LOG: undefined,
// same thing as...
LOG: {
actions: []
}
}
例如,我们模拟所有事件都可以记录 log 数据,只在 userInfoPage 下不可以:
```js {15} const formMachine = createMachine({ id: ‘form’, initial: ‘firstPage’, states: { firstPage: { / … / }, secondPage: { / … / }, userInfoPage: { on: { // 明确禁止 LOG 事件执行任何操作或将任何转换,转换为任何其他状态 LOG: undefined } } }, on: { LOG: { actions: ‘logTelemetry’ } } });
::: tip
请注意,在分层嵌套状态链中定义具有相同事件名称的多个转换时,将只采用最内部的转换。 在上面的例子中,这就是为什么一旦状态机到达 `userInfoPage` 状态,父 `LOG` 事件中定义的 `logTelemetry` 动作就不会执行。
:::
## 多个目标
基于单个事件的转换可以有多个目标状态节点。 这是不常见的,只有在状态节点合法时才有效; 例如,在复合状态节点中,转换到两个兄弟状态节点是非法的,因为(非并行)状态机在任何给定时间只能处于一种状态。
多个目标在 `target: [...]` 中被指定为一个数组,其中数组中的每个目标都是一个状态节点的相对键或 ID,就像单个目标一样。
```js {23}
const settingsMachine = createMachine({
id: 'settings',
type: 'parallel',
states: {
mode: {
initial: 'active',
states: {
inactive: {},
pending: {},
active: {}
}
},
status: {
initial: 'enabled',
states: {
disabled: {},
enabled: {}
}
}
},
on: {
// 多目标
DEACTIVATE: {
target: ['.mode.inactive', '.status.disabled']
}
}
});
通配描述符
使用通配符事件描述符 ("*"
) 指定的转换由任何事件激活。 这意味着 任何事件 都将匹配具有 on: { "*": ... }
的转换,并且如果守卫通过,则将采用该转换。
除非在数组中指定转换,否则将始终选择显式事件描述符而不是通配符事件描述符。 在这种情况下,转换的顺序决定了选择哪个转换。
```js {3,8} // 对于 SOME_EVENT,将显式转换到“here” on: { “*”: “elsewhere”, “SOME_EVENT”: “here” }
// 对于 SOME_EVENT,将采用通配符转换为“elsewhere” on: [ { event: “*”, target: “elsewhere” }, { event: “SOME_EVENT”, target: “here” }, ]
::: tip
通配符描述符的行为方式与 [瞬间转换](#transient-transitions)(具有空事件描述符)_不同_。 每当状态处于活动状态时都会立即进行瞬态转换,而通配符转换仍然需要将某些事件发送到其状态才能触发。
:::
**示例:**
```js {7,8}
const quietMachine = createMachine({
id: 'quiet',
initial: 'idle',
states: {
idle: {
on: {
WHISPER: undefined,
// 在除 WHISPER 之外的任何事件中,转换到 'disturbed' 状态
'*': 'disturbed'
}
},
disturbed: {}
}
});
quietMachine.transition(quietMachine.initialState, { type: 'WHISPER' });
// => State { value: 'idle' }
quietMachine.transition(quietMachine.initialState, { type: 'SOME_EVENT' });
// => State { value: 'disturbed' }
FAQ
如何在转换中执行 if/else 逻辑?
有时,你会想说:
- 如果 something 是真的,就进入这个状态
- 如果 something else 为真,则转到此状态
- 否则,进入这个状态
你可以使用 守卫转换 来实现这一点。
我如何转换到 任何 状态?
你可以通过为该状态提供自定义 ID 并使用 target: '#customId'
来转换到 任何 状态。 你可以在此处阅读有关 自定义 ID 的完整文档。
这允许你从子状态转换到父级的兄弟状态,例如在本例中的 CANCEL
和 done
事件中: