有限状态机

有限状态机

有限状态机 (Finite State Machine, FSM) 是一种数学模型用来描述系统的行为,这个系统在任何时间点上都只会存在于一个状态。举例来说,红绿灯就有 红灯、绿灯、黄灯 三种状态,在任何时间点上一定是这三种状态的其中一种,不可能在一个时间点上存在两种或两种以上的状态。

一个正式的有限状态机包含五个部分:

  • 有限数量的状态 (state)
  • 有限数量的事件 (event)
  • 一个初始状态 (initial state)
  • 一个转换函式 (transition function),传入当前的状态及事件时会返回下一个状态
  • 具有 0 至 n 个最终状态 (final state)

需要强调的是这裡的 状态 (State) 指的是系统定性的 mode 或 status,并不是指系统内所有的资料。举例来说,水有 4 种状态 (State)-固态、液态、气态 以及等离子态,这就属于状态,但水的温度是可变的定量且无限的可能就不属于状态!

建立第一个 Machine

XState 的 Machine 其实就是一个 State Machine (精确地说是 Statechart),所以我们在建立一个 Machine 要先整理我们的,程序有哪些状态,哪些事件,以及初始状态。

import { Machine } from "xstate";

const lightMachine = Machine({
  states: {
    red: {},
    green: {},
    yellow: {},
  },
});

首先我们需要订定 Machine 会有哪些状态,传给 Machine 一个 object 内部必须有 states 这个属性,而 states object 的每个 key 就是这个 Machine 拥有的状态。所以这段,程序码代表这个 Machine 拥有 red, green, yellow 三种状态。

import { Machine } from "xstate";

const lightMachine = Machine({
  states: {
    red: {},
    green: {},
    yellow: {},
  },
});

接下来我们要定义初始状态,假如说我们希望一开始是红灯,那就给 initial 如下:

import { Machine } from "xstate";

const lightMachine = Machine({
  initial: "red",
  states: {
    red: {},
    green: {},
    yellow: {},
  },
});

initial 给 ‘red’ 这样我们的 lightMachine 的初始状态就会是 red。接下来我们要定义每个状态下会有什麽事件,遇到这些事件时,会转换成什麽状态。这裡我们订定三个状态下都会有 CLICK 事件,并且状态的转换是 red -> green -> yellow -> red … 那我们的,程序码就会像下这面这样:

import { Machine } from "xstate";

const lightMachine = Machine({
  initial: "red",
  states: {
    red: {
      on: {
        CLICK: "green",
      },
    },
    green: {
      on: {
        CLICK: "yellow",
      },
    },
    yellow: {
      on: {
        CLICK: "red",
      },
    },
  },
});

我们在每个状态下加入 on 属性,on 的 key 代表事件名称,value 则代表转移的下一个状态。这时候我们就可以拿 lightMachine 来使用了!透过 .transition(state, event) 这个方法来取得下一个状态:

import { Machine } from "xstate";

const lightMachine = Machine({
  //...
});

const state0 = lightMachine.initialState;
console.log(state0);
const state1 = lightMachine.transition(state0, "CLICK");
console.log(state1);
const state2 = lightMachine.transition(state1, "CLICK");
console.log(state2);
const state3 = lightMachine.transition(state2, "CLICK");
console.log(state3);

这个回传的 state object 有两个常用的方法及属性分别是:

  • value
  • matches(parentStateValue)
  • nextEvents

value 可以拿到当前的状态,matches 则可以用来判断现在是否在某个状态,比如说:

import { Machine } from "xstate";

const lightMachine = Machine({
  //...
});

const state0 = lightMachine.initialState;
console.log(state0.value); // 'red'
const state1 = lightMachine.transition(state0, "CLICK");
console.log(state1.value); // 'green'

state0.matches("red"); // true
state0.matches("yellow"); // false
state0.matches("green"); // false

nextEvents 则可以拿到该 state 有哪些 events 可以使用:

import { Machine } from "xstate";

const lightMachine = Machine({
  //...
});

const state0 = lightMachine.initialState;
console.log(state0.nextEvents); // 'CLICK'

这样一来我们就完成了一个简单的 Machine,但我们的 lightMachine 每次都要传入当前的 state 跟 event 才能做状态转换,这是为了让 transition 保持是一个 Pure Function,它不会改变 lightMachine 物件的状态,方便我们做单元测试。但我们通常不想要自己储存及管理状态,所以 XState 提供了 Interpret!

Interpret

XState 提供了一个叫 interpret 的 function 可以把一个 machine 实例转换成一个具有状态的 service,如下:

import { Machine, interpret } from "xstate";

const lightMachine = Machine({
  //...
});

const service = interpret(lightMachine);

// 启动 service
service.start();

// Send events
service.send("CLICK");

// 停止 service 当你不在使用它
service.stop();

interpret 得到的 service 具有自己的状态,当 start() 后,这个 service 就会到初始状态,同时可以对他传送(send)事件,同时也可以透过 service.state 拿到当前的状态,如下

import { Machine, interpret } from "xstate";

const lightMachine = Machine({
  //...
});

const service = interpret(lightMachine);

// 启动 service
service.start();

console.log(service.state.value); // 'red'
service.send("CLICK"); // Send events
console.log(service.state.value); // 'green'

// 停止 service 当你不在使用它
service.stop();

这样一来我们就可以很简单的透过 service 来管理及保存当前的状态!

上一页