# 解释(Interpreting) 状态机

虽然具有纯.transition() 函数的状态机/状态图对于灵活性、纯度和可测试性很有用,但为了使其在实际应用程序中有任何用处,需要:

  • 跟踪当前状态,并坚持下去
  • 执行副作用
  • 处理延迟的转换和事件
  • 与外部服务沟通

解释 负责 解释 状态机/状态图并执行上述所有操作 - 即在运行时环境中解析和执行它。 状态图的解释的、运行的实例称为服务

# 解释(Interpreter) 4.0+

提供了一个可选的解释,你可以使用它来运行状态图。 解释处理:

  • 状态转换
  • 执行动作(副作用)
  • 取消的延迟事件
  • 活动(正在进行的行动)
  • 调用/生成子状态图服务
  • 支持状态转换、上下文更改、事件等的多个监听器。
  • 和更多!
import { createMachine, interpret } from 'xstate';

const machine = createMachine(/* machine config */);

// 解释状态机,并在发生转换时添加一个监听器。
const service = interpret(machine).onTransition((state) => {
  console.log(state.value);
});

// 启动服务
service.start();

// 发送事件
service.send({ type: 'SOME_EVENT' });

// 当你不再使用该服务时,请停止该服务。
service.stop();

# 发送事件

通过调用 service.send(event) 将事件发送到正在运行的服务。 有 3 种方式可以发送事件:





 


 



 

service.start();

// 作为对象(首选):
service.send({ type: 'CLICK', x: 40, y: 21 });

// 作为字符串:
// (与 service.send({ type: 'CLICK' }) 一样)
service.send('CLICK');

// 作为带有对象负载的字符串:
// (同 service.send({ type: 'CLICK', x: 40, y: 21 }))
service.send('CLICK', { x: 40, y: 21 });
  • 作为事件对象(例如,.send({ type: 'CLICK', x: 40, y: 21 })
    • 事件对象必须有一个 type: ... 字符串属性。
  • 作为字符串(例如,.send('CLICK'),它解析为发送 { type: 'CLICK' }
    • 该字符串表示事件类型。
  • 作为后跟对象有效负载的字符串(例如,.send('CLICK', { x: 40, y: 21 })4.5+
    • 第一个字符串参数表示事件类型。
    • 第二个参数必须是一个没有 type: ... 属性的对象。

注意

如果服务未初始化(即,如果尚未调用service.start()),则事件将延迟,直到服务启动。 这意味着在调用 service.start() 之前不会处理事件,然后它们将被顺序处理。

这种行为可以通过在 服务选项 中设置 { deferEvents: false } 来改变。 当 deferEventsfalse 时,向未初始化的服务发送事件将引发错误。

# 批量发送事件

通过使用一组事件调用service.send(events),可以将多个事件作为一个组或“批处理”发送到正在运行的服务:

service.send([
  // 字符串事件
  'CLICK',
  'CLICK',
  'ANOTHER_EVENT',
  // 事件对象
  { type: 'CLICK', x: 40, y: 21 },
  { type: 'KEYDOWN', key: 'Escape' }
]);

这将立即安排要按顺序处理的所有批处理事件。 由于每个事件都会导致可能需要执行操作的状态转换,因此中间状态中的操作会被推迟,直到所有事件都被处理完毕,然后以创建它们的状态(而不是结束状态)执行它们。

这意味着结束状态(在处理完所有事件之后)将有一个 .actions 数组,其中包含来自中间状态 所有 的累积动作。 这些动作中的每一个都将绑定到它们各自的中间状态。

注意

只有一种状态——结束状态(即,处理所有事件后的结果状态)——将被发送到.onTransition(...) 监听器。 这使得批处理事件成为性能的优化方法。

提示

批处理事件对于 事件源 (opens new window) 方法很有用。 通过将批处理事件发送到服务以达到相同的状态,可以存储事件日志并稍后重放。

# 转换

状态转换的监听器通过.onTransition(...) 方法注册,该方法采用状态监听器。 每次发生状态转换(包括初始状态)时都会调用状态监听器,使用当前 state 实例

// 解释状态机
const service = interpret(machine);

// 添加一个状态监听器,每当发生状态转换时都会调用它。
service.onTransition((state) => {
  console.log(state.value);
});

service.start();

提示

如果你只想在状态更改时调用 .onTransition(...) 处理程序(即,当 state.value 更改时,state.context 更改,或者有新的 state.actions),使用 state.changed (opens new window)


 




service.onTransition((state) => {
  if (state.changed) {
    console.log(state.value);
  }
});

提示

.onTransition() 回调不会在无事件(“always”)转换或其他微任务之间运行。 它只在宏任务上运行。 微任务是宏任务之间的中间转换。

# 开始和停止

可以使用.start().stop() 来初始化(即启动)和停止服务。 调用 .start() 将立即将服务转换到其初始状态。 调用 .stop() 将从服务中删除所有监听器,并进行任何监听器清理(如果适用)。

const service = interpret(machine);

// 启动状态机
service.start();

// 停止状态机
service.stop();

// 重启状态机
service.start();

通过将 state 传递给 service.start(state),可以从特定的 状态 启动服务。 这在从先前保存的状态重新混合服务时很有用。

// 从指定状态启动服务,而不是从状态机的初始状态启动。
service.start(previousState);

# 执行动作

动作 (副作用) 默认情况下,在状态转换时立即执行。 这可以通过设置 { execute: false } 选项来配置(参见示例)。 在 state 上指定的每个动作对象可能有一个 .exec 属性,该属性被状态的 contextevent 对象调用。

可以通过调用service.execute(state) 手动执行操作。 当你想要控制执行操作的时间时,这很有用:

const service = interpret(machine, {
  execute: false // 不要对状态转换执行操作
});

service.onTransition((state) => {
  // 在下一动画帧而不是立即执行动作
  requestAnimationFrame(() => service.execute(state));
});

service.start();

# 选项

以下选项可以作为第二个参数传递给解释(interpret(machine, options)):

  • execute (boolean) - 表示是否应在转换时执行状态操作。 默认为 true
  • deferEvents (boolean) 4.4+ - 表示发送到未初始化服务的事件(即在调用 service.start() 之前)是否应该推迟到服务初始化。 默认为 true
    • 如果为 false,则发送到未初始化服务的事件将引发错误。
  • devTools (boolean) - 表示事件是否应该发送到 Redux DevTools 扩展 (opens new window)。 默认为false
  • logger - 指定用于log(...) 操作的记录器。 默认为原生 console.log 方法。
  • clock - 指定延迟操作的时钟接口。 默认为原生 setTimeoutclearTimeout 函数。

# 自定义 解释(Interpreters)

你可以使用任何解释器(或创建你自己的解释器)来运行你的状态机/状态图。 这是一个示例最小实现,它演示了解释的灵活程度(尽管有大量的样板):

const machine = createMachine(/* 状态机配置 */);

// 跟踪当前状态,从初始状态开始
let currentState = machine.initialState;

// 跟踪 监听
const listeners = new Set();

// 有一种发送/调度事件的方法
function send(event) {
  // 记住:machine.transition() 是一个纯函数
  currentState = machine.transition(currentState, event);

  // 获取要执行的副作用操作
  const { actions } = currentState;

  actions.forEach((action) => {
    // 如果动作是可执行的,执行它
    typeof action.exec === 'function' && action.exec();
  });

  // 通知 监听器
  listeners.forEach((listener) => listener(currentState));
}

function listen(listener) {
  listeners.add(listener);
}

function unlisten(listener) {
  listeners.delete(listener);
}

// 现在你可以监听和发送事件以更新状态
listen((state) => {
  console.log(state.value);
});

send('SOME_EVENT');

# 笔记

  • interpret 函数从 4.3+ 开始直接从 xstate 导出(即 import { interpret } from 'xstate')。 对于以前的版本,它是从 'xstate/lib/interpreter' 导入的。
  • 大多数解释器方法都可以链式调用:
const service = interpret(machine)
  .onTransition((state) => console.log(state))
  .onDone(() => console.log('done'))
  .start(); // 返回已启动的服务
  • 不要直接从动作中调用service.send(...)。 这会阻碍测试、可视化和分析。 而是 使用invoke