# 转换 Transitions
转换定义了状态机如何对 事件 做出响应。 要了解更多信息,请参阅 状态图介绍 中的部分。
# API
状态转换在状态节点的 on
属性中定义,:
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' }
, - 转换对象数组,用于条件转换(请参阅 守卫)
# 状态机 .transition
方法
如上所示, machine.transition(...)
方法是一个纯函数,它接受两个参数:
它返回一个新的 State
实例,这是采用当前状态和事件,启用的所有转换的结果。
const lightMachine = createMachine({
/* ... */
});
const greenState = lightMachine.initialState;
// 根据当前状态和事件确定下一个状态
const yellowState = lightMachine.transition(greenState, { type: 'TIMER' });
console.log(yellowState.value);
// => 'yellow'
# 选择启用转换
启用的转换 是将根据当前状态和事件有条件地进行的转换。 当且仅当:
在 分层状态机 中,转换的优先级取决于它们在树中的深度; 更深层次的转换更具体,因此具有更高的优先级。 这与 DOM 事件的工作方式类似:如果单击按钮,则直接在按钮上的单击事件处理程序比 window
上的单击事件处理程序更具体。
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
属性:
// ...
{
on: {
// "CLICK"是事件描述符。
// 此转换匹配具有 { type: 'CLICK' } 的事件
CLICK: 'someState',
// "SUBMIT"是事件描述符。
// 此转换匹配具有 { type: 'SUBMIT' } 的事件
SUBMIT: 'anotherState'
}
}
// ...
其他事件描述符包括:
- Null 事件描述 (
""
),不匹配任何事件(即 "null" 事件),并表示进入状态后立即进行的转换 - 通配符事件描述 (
"*"
) 4.7+,如果事件没有被状态中的任何其他转换显式匹配,则匹配任何事件
# 自转换
自转换是当一个状态转换到自身时,它 可以 退出然后重新进入自身。 自转换可以是 内部 或 外部 转换:
- 内部转换 不会退出也不会重新进入自身,但可能会进入不同的子状态。
- 外部转换 将退出并重新进入自身,也可能退出/进入子状态。
默认情况下,具有指定目标的所有转换都是外部的。
有关如何在自转换上执行进入/退出操作的更多详细信息,请参阅有关 自转换的操作。
# 内部转换
内部转换是不退出其状态节点的转换。 内部转换是通过指定 相对目标(例如,'.left'
)或通过在转换上显式设置 { internal: true }
来创建的。 例如,考虑一台状态机将一段文本设置为对齐 'left'
、 'right'
、 'center'
、或 'justify'
:
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
)的转换也是内部转换:
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 }
来实现。
// ...
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 }
- 外部自转换,同上
# 瞬间转换
注意
空字符串语法 ({ on: { '': ... } }
) 将在第 5 版中弃用。应该首选 4.11+ 版中新的 always
语法。请参阅下面关于 无事件转换 的部分,它与瞬间转换相同。
瞬间转换是由 null 事件 触发的转换。 换句话说,只要满足任何条件,就会 立即 进行转换(即,没有触发事件):
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") 转换 4.11+
无事件转换,是当状态机处于定义的状态,并且其 cond
守卫为 true
时 始终进行 的转换。 他们被检查:
- 立即进入状态节点
- 每次状态机接收到一个可操作的事件(无论该事件是触发内部转换还是外部转换)
无事件转换在状态节点的 always
属性上定义:
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. 通配符转换
- 通配符转换 在进入状态节点时不被检查。 无事件转换是,在做任何其他事情之前(甚至在进入动作的守卫判断之前)的转换。
- 无事件转换的重新判断,由任何可操作的事件触发。 通配符转换的重新判断,仅由与显式事件描述符不匹配的事件触发。
注意
如果误用无事件转换,则有可能创建无限循环。
无事件转换应该使用 target
、cond
+ target
、cond
+ actions
或 cond
+ target
+ actions
来定义。 目标(如果已声明)应与当前状态节点不同。 没有 target
和 cond
的无事件转换将导致无限循环。 如果 cond
守卫不断返回 true
,则带有 cond
和 actions
的转换可能会陷入无限循环。
提示
当检查无事件转换时,它们的守卫会被重复判断,直到它们都返回 false,或者验证了具有目标的转换。 在此过程中,每当某个守卫判断为 true
时,其关联的操作将被执行一次。 因此,在单个微任务期间,可能会多次执行一些没有目标的转换。这与普通转换形成对比,在普通转换中,最多只能进行一个转换。
# 禁止转换
在 XState 中,“禁止”转换是一种指定不应随指定事件发生状态转换的转换。 也就是说,在禁止转换上不应发生任何事情,并且该事件不应由父状态节点处理。
通过将 target
明确指定为 undefined
来进行禁止转换。 这与将其指定为没有操作的内部转换相同:
on: {
// 禁止转换
LOG: undefined,
// same thing as...
LOG: {
actions: []
}
}
例如,我们模拟所有事件都可以记录 log 数据,只在 userInfoPage 下不可以:
const formMachine = createMachine({
id: 'form',
initial: 'firstPage',
states: {
firstPage: {
/* ... */
},
secondPage: {
/* ... */
},
userInfoPage: {
on: {
// 明确禁止 LOG 事件执行任何操作或将任何转换,转换为任何其他状态
LOG: undefined
}
}
},
on: {
LOG: {
actions: 'logTelemetry'
}
}
});
提示
请注意,在分层嵌套状态链中定义具有相同事件名称的多个转换时,将只采用最内部的转换。 在上面的例子中,这就是为什么一旦状态机到达 userInfoPage
状态,父 LOG
事件中定义的 logTelemetry
动作就不会执行。
# 多个目标
基于单个事件的转换可以有多个目标状态节点。 这是不常见的,只有在状态节点合法时才有效; 例如,在复合状态节点中,转换到两个兄弟状态节点是非法的,因为(非并行)状态机在任何给定时间只能处于一种状态。
多个目标在 target: [...]
中被指定为一个数组,其中数组中的每个目标都是一个状态节点的相对键或 ID,就像单个目标一样。
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']
}
}
});
# 通配描述符 4.7+
使用通配符事件描述符 ("*"
) 指定的转换由任何事件激活。 这意味着 任何事件 都将匹配具有 on: { "*": ... }
的转换,并且如果守卫通过,则将采用该转换。
除非在数组中指定转换,否则将始终选择显式事件描述符而不是通配符事件描述符。 在这种情况下,转换的顺序决定了选择哪个转换。
// 对于 SOME_EVENT,将显式转换到“here”
on: {
"*": "elsewhere",
"SOME_EVENT": "here"
}
// 对于 SOME_EVENT,将采用通配符转换为“elsewhere”
on: [
{ event: "*", target: "elsewhere" },
{ event: "SOME_EVENT", target: "here" },
]
提示
通配符描述符的行为方式与 瞬间转换(具有空事件描述符)不同。 每当状态处于活动状态时都会立即进行瞬态转换,而通配符转换仍然需要将某些事件发送到其状态才能触发。
示例:
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
事件中: