十分钟学会有限状态机的实现原理
在学习状态机实现原理之前,我们先了解状态机的五个定义如下:
定义一:有限的状态
定义二:有限的事件
定义三:一个初始状态
定义四:变换器(给定当前状态 + 事件,可以得出下个状态)
定义五:若干个(或无)的最终状态
然后我们依据状态机的定义进行逐个分析。
定义解读
定义一:有限的状态
状态机之所以这样命名,大略都可以理解为管理“状态”的机器。从工业生产可用性的角度来看,任何事物都是可以近似看做有限的状态的。
怎么理解呢?比如一杯水的温度可以算作它的状态,理论上温度的值是无限的,比如我可以说水的温度从 30° 升到了 30.0001° 也算是状态变化,但是我们在普遍场景下,对水的温度精度并不会要求很高。
因此我们可以定义为水的每 1° 就是一个状态,并且我们只关注 0° ~ 100°。因此水的温度状态就只有 100 个,从而表达了有限的概念。
对于设计实现来说,我们拿灯泡举例子,假设灯泡有亮、不亮、坏掉三个状态,我们仅仅用一个数组就可以表达完这个事情。
const lightBulbStates = ['LIT', 'UNLIT', 'BROKEN'];复制代码
当然,在实际开发中,我们也可以用哈希表或者 enum 枚举所有状态。
const lightBulbStates = { lit: 'LIT', unlit: 'UNLIT', broken: 'BROKEN', };复制代码
定义二:有限的事件
什么是事件?
举个例子:比如说小明心如止水地在工作,突然隔壁同事给了他一巴掌,小明气炸了,准备撸起袖子去打架。
这里小明就是一个状态机,他从心如止水的状态,转换到了气炸了的状态,全因发生了被同事扇了一巴掌的事件,并且产生了准备去干架的行为副作用。公式如下:
心如止水(状态) + 被扇一巴掌(事件) = 气炸了(新的状态)+ 准备干架(副作用)。
当然同事之间还是要团结友爱啦,君子动口不动手,我们要以理服人,是吧。
基于上述的例子,我们进行抽象,总结得出公式如下:
state + event = newState + effect复制代码
因此,因为状态是有限的,所以当然事件也是有限的。就比如图有三个顶点,点之间的连线如果有向,那也只有六条线,这很好理解。
再结合到我们的灯泡状态机来看,它的事件有如下几种:
unlit + TOGGLE = lit + effect lit + TOGGLE = unlit + effect unlit + BREAK = broken + effect lit + BREAK = broken + effect复制代码
我们整理一下,用 JavaScript 对象来表述这个事情:
const lightBulbEventMap = { unlit: { on: { TOGGLE: 'lit', BREAK: 'broken', }, }, lit: { on: { TOGGLE: 'unlit', BREAK: 'broken', }, }, broken: {}, };复制代码
这样状态与事件之间的关系就一目了然了。
定义三:一个初始状态
盘古开天、女娲造人、伊甸园里的亚当夏娃,万事万物都有一个初始状态。就像人的一生也会从呱呱落地开始,到化作一抔黄土结束,整个过程是随时间流逝而不断变换的,我们不可能直接绕过开始,直接到中间某个节点。
对于状态机来说也是如此。灯泡买回来一般处于未点亮的状态(正常来说)。因此,我们还是用 JavaScript 对象来表述这个事情:
const lightBulbMachine = { initial: 'unlit', states: { unlit: { on: { TOGGLE: 'lit', BREAK: 'broken', }, }, lit: { on: { TOGGLE: 'unlit', BREAK: 'broken', }, }, broken: {}, }, };复制代码
定义四:变换器(给定当前状态 + 事件,可以得出下个状态)
其实到这里我们已经把灯泡的状态、事件都定义完了。但我们仍然需要给出变换器的实现,用以实现状态与状态之前的过渡。
变换器实现复杂么?不,一点也不。请看:
function transition(state, event) { return machine.states[state].on[event]; }复制代码
然后我们可以用变换器函数对灯泡的状态进行运算
const newState = transition('unlit', 'TOGGLE'); // lit复制代码
或者~
const newState = transition('lit', 'BREAK'); // broken复制代码
假如灯泡坏了,我们还是想点亮它,怎么办?
const newState = transition('broken', 'TOGGLE'); // Throw error复制代码
状态机告诉你,痴心妄想!
定义五:若干个(或无)的最终状态
前面说到万事万物都有起点,所以按道理来说,应该也必然有终点。但是考虑到实际应用中,其实抽象来说未必需要一个终点。就比如网站用户可以登录、漫游、退出、再登录、再漫游等等,它并没有一个实际的终点。
但是也可以有终点。比如 Promise 请求,从 init 到 pending,再返回 resolve 代表成功,或者 reject 代表失败。它有两个终点。
比如人从出生到逝去,宏观上来说,分别对应着初始状态、最终状态。
我们还是回到我们的灯泡状态机,broken 就是它的最终状态。状态机一旦抵达到最终的状态,就相当于结束了。
const lightBulbMachine = { initial: 'unlit', states: { // ... broken: { type: 'final', }, }, };复制代码
状态机最简单实现
来,让我们实现一个状态机吧!
function createMachine({ initial, states }) { const machine = { initialState: initial, transition: (state, event) => states[state].on[event] }; return machine; }复制代码
同学们肯定会觉得,就这么简单么?其实本质上就这样。虽然如此简单的实现,是绝对不能支撑起我们复杂的业务场景需求的。但是一旦只要明白了本质,扩展的实现理解起来也不会很困难。
副作用
上文的状态机实现其实本质上是个纯函数,但我们仔细观察上文提到过的公式。
state + event = newState + effect复制代码
关于副作用我们却只字未提。在大型的应用中,副作用是到处存在的,比如网络请求、本地日志存储、错误输出、DOM 操作、数据库写入、事件注册监听。
纯函数可以提高我们代码的质量,但是却不能表达复杂的业务。
状态机也是如此。状态机可以通过注册活动(Activities)、上下文(context)、订阅状态机服务(Service.subscribe)等方式进行表达副作用。
这些都可以去 XState Docs XState 官网进行学习了解。
源码解读
这里贴上了最简单的状态机实现(@xstate/fsm)的注释。除了包含了上述最基本的五个定义、还有扩展上下文(context)、条件守护(cond)、状态机解释服务(service)等。
对状态机更进一步实现感兴趣的同学可以阅读。
// xstate.jsimport { toTransitionObject, toActionObject, toEventObject, toArray, isString, isFunction } from './util';const INIT_EVENT = { type: 'xstate.init' };const ASSIGN_ACTION = 'xstate.assign';function createMatcher(value) { return stateValue => value === stateValue; }function createUnchangedState(value, context) { return { value, context, actions: [], changed: false, matches: createMatcher(value) }; }export function assign(assignment) { return { type: ASSIGN_ACTION, assignment } }export function execActions(state, event) { state.actions.forEach(({ exec }) => { exec && exec(state.context, event); }); }// 通过约定的状态机配置,生产一个有限状态机// 状态机定义如下:// - 定义一:有限的状态// - 定义二:有限的事件// - 定义三:一个初始状态// - 定义四:变换器(给定当前状态 + 事件,可以得出下个状态)// - 定义五:若干个(或无)的最终状态export function createMachine(fsmConfig, options = {}) { function getCombinedActions({ states, initial }) { const entryAction = states[initial].entry; return toArray( entryAction ).map(action => toActionObject(action, options.actions)); } function getContext() { return fsmConfig.context || {}; } const machine = { _options: options, config: fsmConfig, // 实现定义三 - 一个初始状态节点 initialState: { // 状态值 value: fsmConfig.initial, // 事件 actions: getCombinedActions(fsmConfig), // 上下文 context: getContext(), // 状态匹配判断方法 matches: createMatcher(fsmConfig.initial) }, // 实现定义四 - 变换器 transition: (state, event) => { const { value, context } = isString(state) ? { value: state, context: getContext() } : state; const eventObject = toEventObject(event); const stateConfig = fsmConfig.states[value]; if (stateConfig.on) { // 根据事件类型取得对应的变换器 const transitions = toArray(stateConfig.on[eventObject.type]); for (const transtion of transitions) { // 实际上没有任何变换器、或使用 undefined 中断了变换器 if (transtion === undefined) { return createUnchangedState(value, context); } const { target = value, actions = [], cond = () => true } = toTransitionObject(transtion); let nextContext = context; // 条件守护,只要满足一个变换器,直接退出 if (cond(context, eventObject)) { // 判断上下文是否被修改 let assigned = false; // 获取下个状态对象节点 const nextStateConfig = fsmConfig.states[target]; const allActions = [] // 依次进行当前状态的 Exit Action、当前状态的 Action 集合、下个状态的 Entry Action .concat( stateConfig.exit, actions, nextStateConfig.entry ) // 过滤假值 .filter(a => a) // 寻找 Action 配置,返回统一格式的 Action 对象 .map(action => toActionObject(action, machine._options.actions)) // 内部事件,支持 assign 函数修改上下文 .filter(action => { if (action.type === ASSIGN_ACTION) { assigned = true; let tmpContext = Object.assign({}, nextContext); if (isFunction(action.assignment)) { tmpContext = action.assignment(nextContext, eventObject); } else { Object.keys(action.assignment).forEach(key => { const assignment = action.assignment[key]; tmpContext[key] = isFunction(assignment) ? assignment(nextContext, eventObject) : assignment }); } nextContext = tmpContext; return false; } return true; }); return { value: target, context: nextContext, actions: allActions, changed: target !== value || allActions.length > 0 || assigned, matches: createMatcher(target) }; } } } // 状态节点未注册任何变换器、条件守护未满足,导致停留在原本的状态节点 return createUnchangedState(value, context); } }; return machine; }// 状态机服务的状态export const INTERPRETER_STATUS = { NotStarted: 0, Running: 1, Stopped: 2};// 将 Machine 解释为一个状态机服务,应用于实际生产环境export function interpret(machine) { let state = machine.initialState; let status = INTERPRETER_STATUS.NotStarted; // 用以订阅状态机服务 const listeners = new Set(); const service = { _machine: machine, send(event) { // 状态机服务未启动时,不允许发送事件 if (status !== INTERPRETER_STATUS.Running) { return; } // 通过变换器更新状态 state = machine.transition(state, event); // 执行当前状态的所有 Action(Action 集合在 Machine 中已进行计算) execActions(state, toEventObject(event)); listeners.forEach((listener) => listener(state)); return service; }, subscribe: (listener) => { listeners.add(listener); listener(state); return { unsubscribe: () => listeners.delete(listener) }; }, // 状态机服务启动 start: () => { status = INTERPRETER_STATUS.Running; execActions(state, INIT_EVENT); return service; }, // 状态机服务暂停 stop: () => { status = INTERPRETER_STATUS.Stopped; listeners.clear(); return service; }, get state() { return state; }, get status() { return status; } }; return service; }export const Machine = createMachine;复制代码
// util.jsexport function toArray(item) { return item === undefined ? [] : [].concat(item); }export function isString(o) { return typeof o === 'string'; }export function isFunction(o) { return typeof o === 'function'; }export function toActionObject(action, actionMap = {}) { action = isString(action) && actionMap[action] ? actionMap[action] : action; if (isString(action)) { return { type: action }; } else if (isFunction(action)) { return { type: action.name, exec: action } } else { return action; } }export function toEventObject(event) { return isString(event) ? { type: event } : event; }export function toTransitionObject(transition) { return isString(transition) ? { target: transition } : transition; }复制代码