转换 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’,

  1. // 状态转换 (object)
  2. REJECT: {
  3. target: 'rejected'
  4. }
  5. }
  6. },
  7. resolved: {
  8. type: 'final'
  9. },
  10. rejected: {
  11. type: 'final'
  12. }

} });

const { initialState } = promiseMachine;

console.log(initialState.value); // => ‘pending’

const nextState = promiseMachine.transition(initialState, { type: ‘RESOLVE’ });

console.log(nextState.value); // => ‘resolved’

  1. 在上面的例子中,当状态机处于 `pending` 状态并且它接收到一个 `RESOLVE` 事件时,它会转换到 `resolved` 状态。
  2. 状态转换可以定义为:
  3. - 一个字符串,例如 `RESOLVE: 'resolved'`
  4. - 具有 `target` 属性的对象,例如 `RESOLVE: { target: 'resolved' }`,
  5. - 转换对象数组,用于条件转换(请参阅 [守卫]($zh-guides-guards.md))
  6. ## 状态机 `.transition` 方法
  7. 如上所示, `machine.transition(...)` 方法是一个纯函数,它接受两个参数:
  8. - `state` - 要转换的 [状态](./states.md)
  9. - `event` - 导致转换的 [事件]($zh-guides-events.md)
  10. 它返回一个新的 [`State` 实例](./states.md#state-definition),这是采用当前状态和事件,启用的所有转换的结果。
  11. ```js {8}
  12. const lightMachine = createMachine({
  13. /* ... */
  14. });
  15. const greenState = lightMachine.initialState;
  16. // 根据当前状态和事件确定下一个状态
  17. const yellowState = lightMachine.transition(greenState, { type: 'TIMER' });
  18. console.log(yellowState.value);
  19. // => 'yellow'

选择启用转换

启用的转换 是将根据当前状态和事件有条件地进行的转换。 当且仅当:

  • 它在与当前状态值匹配的 状态节点 上定义
  • 转换 守卫cond 属性)得到条件满足(为 true
  • 它不会被更具体的 转换 所取代。

分层状态机 中,转换的优先级取决于它们在树中的深度; 更深层次的转换更具体,因此具有更高的优先级。 这与 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’

  1. ## 事件描述符
  2. 事件描述符,是描述转换 将匹配的事件类型的字符串。 通常,这等效于发送到状态机的 `event` 对象上的 `event.type` 属性:
  3. ```js
  4. // ...
  5. {
  6. on: {
  7. // "CLICK"是事件描述符。
  8. // 此转换匹配具有 { type: 'CLICK' } 的事件
  9. CLICK: 'someState',
  10. // "SUBMIT"是事件描述符。
  11. // 此转换匹配具有 { type: 'SUBMIT' } 的事件
  12. SUBMIT: 'anotherState'
  13. }
  14. }
  15. // ...

其他事件描述符包括:

  • Null 事件描述 (""),不匹配任何事件(即 “null” 事件),并表示进入状态后立即进行的转换
  • 通配符事件描述 ("*") ,如果事件没有被状态中的任何其他转换显式匹配,则匹配任何事件

自转换

自转换是当一个状态转换到自身时,它 可以 退出然后重新进入自身。 自转换可以是 内部外部 转换:

  • 内部转换 不会退出也不会重新进入自身,但可能会进入不同的子状态。
  • 外部转换 将退出并重新进入自身,也可能退出/进入子状态。

默认情况下,具有指定目标的所有转换都是外部的。

有关如何在自转换上执行进入/退出操作的更多详细信息,请参阅有关 自转换的操作

内部转换

内部转换是不退出其状态节点的转换。 内部转换是通过指定 相对目标(例如,'.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’ } });

  1. 上面的状态机将以 `'left'` 状态启动,并根据单击的内容在内部转换到其他子状态。 此外,由于转换是内部的,因此不会再次执行在父状态节点上定义的 `entry`, `exit` 或者任何其他的 `actions`
  2. 具有 `{ target: undefined }` (或无 `target`)的转换也是内部转换:
  3. ```js {11-13}
  4. const buttonMachine = createMachine({
  5. id: 'button',
  6. initial: 'inactive',
  7. states: {
  8. inactive: {
  9. on: { PUSH: 'active' }
  10. },
  11. active: {
  12. on: {
  13. // 无 target - 内部转换
  14. PUSH: {
  15. actions: 'logPushed'
  16. }
  17. }
  18. }
  19. }
  20. });

内部转换摘要:

  • EVENT: '.foo' - 内部转换到子状态
  • EVENT: { target: '.foo' } - 内部转换到子状态(以'.'开头)
  • EVENT: undefined - 禁止转换
  • EVENT: { actions: [ ... ] } - 内部自转换
  • EVENT: { actions: [ ... ], internal: true } - 内部自转换,同上
  • EVENT: { target: undefined, actions: [ ... ] } - 内部自转换,同上

外部转换

外部转换 退出并重新进入定义转换的状态节点。 在上面的例子中,父级 word 状态节点(根状态节点),将在其转换时执行 exitentry 动作。

默认情况下,转换是外部的,但任何转换都可以通过在转换上显式设置 { 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’ } // …

  1. 上面的每个转换都是外部的,并且将执行父状态的 `exit` `entry` 操作。
  2. **外部转换摘要:**
  3. - `EVENT: { target: 'foo' }` - 所有对兄弟状态的转换都是外部转换
  4. - `EVENT: { target: '#someTarget' }` - 到其他节点的所有转换都是外部转换
  5. - `EVENT: { target: 'same.foo' }` - 外部转换到自己的子级节点(相当于`{ target: '.foo', internal: false }`
  6. - `EVENT: { target: '.foo', internal: false }` - 外部转换到子节点
  7. - 否则这将是一个内部转换
  8. - `EVENT: { actions: [ ... ], internal: false }` - 外部自转换
  9. - `EVENT: { target: undefined, actions: [ ... ], internal: false }` - 外部自转换,同上
  10. ## 瞬间转换
  11. ::: warning
  12. 空字符串语法 (`{ on: { '': ... } }`) 将在第 5 版中弃用。应该首选 4.11+ 版中新的 `always` 语法。请参阅下面关于 [无事件转换](#eventless-always-transitions) 的部分,它与瞬间转换相同。
  13. :::
  14. 瞬间转换是由 [null 事件]($zh-guides-events.md#null-events) 触发的转换。 换句话说,只要满足任何条件,就会 _立即_ 进行转换(即,没有触发事件):
  15. ```js {14-17}
  16. const gameMachine = createMachine(
  17. {
  18. id: 'game',
  19. initial: 'playing',
  20. context: {
  21. points: 0
  22. },
  23. states: {
  24. playing: {
  25. on: {
  26. // 瞬间转换 如果满足条件,将在(重新)进入 'playing' 状态后立即转换为 'win' 或 'lose'。
  27. '': [
  28. { target: 'win', cond: 'didPlayerWin' },
  29. { target: 'lose', cond: 'didPlayerLose' }
  30. ],
  31. // 自转换
  32. AWARD_POINTS: {
  33. actions: assign({
  34. points: 100
  35. })
  36. }
  37. }
  38. },
  39. win: { type: 'final' },
  40. lose: { type: 'final' }
  41. }
  42. },
  43. {
  44. guards: {
  45. didPlayerWin: (context, event) => {
  46. // 检查玩家是否赢了
  47. return context.points > 99;
  48. },
  49. didPlayerLose: (context, event) => {
  50. // 检查玩家是否输了
  51. return context.points < 0;
  52. }
  53. }
  54. }
  55. );
  56. const gameService = interpret(gameMachine)
  57. .onTransition((state) => console.log(state.value))
  58. .start();
  59. // 仍处于 'playing' 状态,因为不满足瞬间转换条件
  60. // => 'playing'
  61. // 当发送“AWARD_POINTS”时,会发生自我转换到“PLAYING”。
  62. // 由于满足“didPlayerWin”条件,因此会进行到“win”的瞬间转换。
  63. gameService.send('AWARD_POINTS');
  64. // => '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’

  1. ### 无事件 vs. 通配符转换
  2. - [通配符转换](#wildcard-descriptors) 在进入状态节点时不被检查。 无事件转换是,在做任何其他事情之前(甚至在进入动作的守卫判断之前)的转换。
  3. - 无事件转换的重新判断,由任何可操作的事件触发。 通配符转换的重新判断,仅由与显式事件描述符不匹配的事件触发。
  4. ::: warning
  5. 如果误用无事件转换,则有可能创建无限循环。
  6. 无事件转换应该使用 `target``cond` + `target``cond` + `actions` `cond` + `target` + `actions` 来定义。 目标(如果已声明)应与当前状态节点不同。 没有 `target` `cond` 的无事件转换将导致无限循环。 如果 `cond` 守卫不断返回 `true`,则带有 `cond` `actions` 的转换可能会陷入无限循环。
  7. :::
  8. ::: tip
  9. 当检查无事件转换时,它们的守卫会被重复判断,直到它们都返回 false,或者验证了具有目标的转换。 在此过程中,每当某个守卫判断为 `true` 时,其关联的操作将被执行一次。 因此,在单个微任务期间,可能会多次执行一些没有目标的转换。这与普通转换形成对比,在普通转换中,最多只能进行一个转换。
  10. :::
  11. ## 禁止转换
  12. XState 中,“禁止”转换是一种指定不应随指定事件发生状态转换的转换。 也就是说,在禁止转换上不应发生任何事情,并且该事件不应由父状态节点处理。
  13. 通过将 `target` 明确指定为 `undefined` 来进行禁止转换。 这与将其指定为没有操作的内部转换相同:
  14. ```js {3}
  15. on: {
  16. // 禁止转换
  17. LOG: undefined,
  18. // same thing as...
  19. LOG: {
  20. actions: []
  21. }
  22. }

例如,我们模拟所有事件都可以记录 log 数据,只在 userInfoPage 下不可以:

```js {15} const formMachine = createMachine({ id: ‘form’, initial: ‘firstPage’, states: { firstPage: { // }, secondPage: { // }, userInfoPage: { on: { // 明确禁止 LOG 事件执行任何操作或将任何转换,转换为任何其他状态 LOG: undefined } } }, on: { LOG: { actions: ‘logTelemetry’ } } });

  1. ::: tip
  2. 请注意,在分层嵌套状态链中定义具有相同事件名称的多个转换时,将只采用最内部的转换。 在上面的例子中,这就是为什么一旦状态机到达 `userInfoPage` 状态,父 `LOG` 事件中定义的 `logTelemetry` 动作就不会执行。
  3. :::
  4. ## 多个目标
  5. 基于单个事件的转换可以有多个目标状态节点。 这是不常见的,只有在状态节点合法时才有效; 例如,在复合状态节点中,转换到两个兄弟状态节点是非法的,因为(非并行)状态机在任何给定时间只能处于一种状态。
  6. 多个目标在 `target: [...]` 中被指定为一个数组,其中数组中的每个目标都是一个状态节点的相对键或 ID,就像单个目标一样。
  7. ```js {23}
  8. const settingsMachine = createMachine({
  9. id: 'settings',
  10. type: 'parallel',
  11. states: {
  12. mode: {
  13. initial: 'active',
  14. states: {
  15. inactive: {},
  16. pending: {},
  17. active: {}
  18. }
  19. },
  20. status: {
  21. initial: 'enabled',
  22. states: {
  23. disabled: {},
  24. enabled: {}
  25. }
  26. }
  27. },
  28. on: {
  29. // 多目标
  30. DEACTIVATE: {
  31. target: ['.mode.inactive', '.status.disabled']
  32. }
  33. }
  34. });

通配描述符

使用通配符事件描述符 ("*") 指定的转换由任何事件激活。 这意味着 任何事件 都将匹配具有 on: { "*": ... } 的转换,并且如果守卫通过,则将采用该转换。

除非在数组中指定转换,否则将始终选择显式事件描述符而不是通配符事件描述符。 在这种情况下,转换的顺序决定了选择哪个转换。

```js {3,8} // 对于 SOME_EVENT,将显式转换到“here” on: { “*”: “elsewhere”, “SOME_EVENT”: “here” }

// 对于 SOME_EVENT,将采用通配符转换为“elsewhere” on: [ { event: “*”, target: “elsewhere” }, { event: “SOME_EVENT”, target: “here” }, ]

  1. ::: tip
  2. 通配符描述符的行为方式与 [瞬间转换](#transient-transitions)(具有空事件描述符)_不同_ 每当状态处于活动状态时都会立即进行瞬态转换,而通配符转换仍然需要将某些事件发送到其状态才能触发。
  3. :::
  4. **示例:**
  5. ```js {7,8}
  6. const quietMachine = createMachine({
  7. id: 'quiet',
  8. initial: 'idle',
  9. states: {
  10. idle: {
  11. on: {
  12. WHISPER: undefined,
  13. // 在除 WHISPER 之外的任何事件中,转换到 'disturbed' 状态
  14. '*': 'disturbed'
  15. }
  16. },
  17. disturbed: {}
  18. }
  19. });
  20. quietMachine.transition(quietMachine.initialState, { type: 'WHISPER' });
  21. // => State { value: 'idle' }
  22. quietMachine.transition(quietMachine.initialState, { type: 'SOME_EVENT' });
  23. // => State { value: 'disturbed' }

FAQ

如何在转换中执行 if/else 逻辑?

有时,你会想说:

  • 如果 something 是真的,就进入这个状态
  • 如果 something else 为真,则转到此状态
  • 否则,进入这个状态

你可以使用 守卫转换 来实现这一点。

我如何转换到 任何 状态?

你可以通过为该状态提供自定义 ID 并使用 target: '#customId' 来转换到 任何 状态。 你可以在此处阅读有关 自定义 ID 的完整文档

这允许你从子状态转换到父级的兄弟状态,例如在本例中的 CANCELdone 事件中: