- Published on
分层状态机(HSM)及其前端实现
- Authors
- Name
- 文森
- Github
- @leungwensen
状态机 (FSM - Finite-State Machine) 是计算理论中的基础概念,也是编程语言和编译器设计的基石。它描述了具有有限状态的系统,以及在不同状态之间进行转换的行为。
状态机的基本概念
以旋转栅门为例,它具有两种状态:上锁 和 解锁。系统可以接收两种事件:投币 和 推门。根据当前状态和事件,系统会转换到新的状态。
状态机的关键要素:
- 有限状态集:例如,上锁/解锁
- 有限事件集:例如,投币/推门
- 初始状态:例如,上锁
- 转换器:定义状态转换规则,根据当前状态和事件确定下一个状态
- 终态(可选): 系统最终停留的状态
状态机的应用场景
状态机广泛应用于对象状态和行为建模,例如:
- 硬件电路系统设计
- 编译器实现
- 网络协议
- 游戏 AI
在 verilog 中创建的有限状态机
一个简单的词法分析器状态机
TCP state diagram
游戏 NPC 状态机
在编程中,使用状态机可以清晰地表达实体的状态和行为,避免复杂的条件判断。
分层状态机 (HSM)
分层状态机 (HSM - Hierarchical State Machine) 在 FSM 的基础上增加了层级结构,可以更好地组织和管理复杂的状态逻辑。子状态的进入和退出会触发父状态的相应事件。
前端实现方式
目前主流的前端 HSM 实现方案有两种:
- xstate: 以状态为中心,功能强大,支持 FSM、HSM、PSM (并行状态机) 等,并提供可视化工具 stately.ai/viz。
- javascript-state-machine: 以转换事件为中心,相对简单,但项目迭代和社区活跃度不如 xstate。
HSM 基础实现示例
以下代码展示了 HSM 的基础实现:
class State {
constructor(config = {}, parent = null) {
const { id, from = '*', onEnter, onExit } = config;
this.id = id;
if (isString(from)) {
if (from === '*') {
this.acceptFromAll = true;
} else {
this.from = [from];
}
} else if (isArray(from)) {
this.from = from;
}
this.onEnter = isFunction(onEnter) ? onEnter : noop;
this.onExit = isFunction(onExit) ? onExit : noop;
this._parent = null;
this.children = [];
this.setParent(parent);
}
get parent() { return this._parent; }
get root() {
let node = this;
while (node._parent instanceof State) {
node = node._prarent;
}
return node;
}
get isRoot() { return this === this.root; }
get ancestors() {
const ancestors = [];
let node = this._parent;
while (node.parent instanceof State) {
ancestors.push(node);
node = node.parent;
}
return ancestors;
}
setParent(parent) {
if (!(parent instanceof State)) return;
if (parent.ancestors.indexOf(this) > -1) {
throw new Error(`cannot set child state ${parent.id} as parent`)
}
if (this.parent instanceof State && this.parent !== parent) {
this.parent.removeChild(this);
}
this._parent = parent;
parent.children.push(this);
}
removeChild(child) {
removeFrom(this.children, child);
child._parent = null;
}
}
module.exports = State;
const State = require('./state');
const EVENTS = {
TRANSITION_COMPLETE: 'TRANSITION_COMPLETE',
TRANSITION_DENIED: 'TRANSITION_DENIED',
};
class StateMachine extends EventEmitter {
static EVENTS = EVENTS;
static State = State;
constructor() {
super();
this._states = {};
this._currentState = null;
}
get states() { return this._states; }
get currentState() { return this._currentState; }
hasState(id) { return keys(this.states).indexOf(id) !== -1; }
getStateById(id) { return this.states[id] || null; }
addState(data = {}, parent = null) {
const { id } = data;
if (this.hasState(id)) {
throw new Error(`state ${id} exists`);
}
parent = parent instanceof State ? parent : this.getStateById(parent);
const state = new State(data, parent);
this._states[id] = state;
}
setInitialState(id) {
if (isNil(this._currentState) && this.hasState(id)) {
this._currentState = id;
const state = this._states[id];
// invoke onEnter call for all ancestors down from
// root parent of the state we're transitioning into
const { root, ancestors, onEnter } = state;
if (!isNil(root)) {
ancestors.forEach((ancestor) => {
if (isFunction(ancestor.onEnter)) {
ancestor.onEnter.call(this, {
toState: id,
});
}
});
}
// invoke onEnter call for id
if (isFunction(onEnter)) {
onEnter.call(this, {
currentState: id,
});
}
this.emit(EVENTS.TRANSITION_COMPLETE, {
toState: id,
});
}
}
canChangeStateTo(id) {
// FIXME: allow from itself?
if (id === this._currentState) return false;
const toState = this.getStateById(id);
if (toState.acceptFromAll) return true;
if (toState.from.indexOf(this._currentState) > -1) return true;
return false;
}
changeState(id) {
if (!this.hasState(id)) {
console.warn(`cannot make transition. state: ${id} is not defined`);
return;
}
const toState = this.getStateById(id);
if (!this.canChangeStateTo(id)) {
console.warn(`transition to state: ${id} is denied`);
this.emit(EVENTS.TRANSITION_DENIED, {
fromState: this._currentState,
toState: id,
allowedStates: toState.from,
});
return;
}
// invoke onExit callback for current state and it's ancestors
// TODO: exceptions: if the target state is still in the same parent,
// do not invoke onExit callback of the parent state
const { currentState } = this;
const allExitingStates = [currentState, ...currentState.ancestors];
for (let i = 0; i < allExitingStates.length; i += 1) {
const state = allExitingStates[i];
if (isFunction(state.onExit)) {
state.onExit.call(this, {
currentState: state.id,
});
}
}
// invoke onEnter callback for target state and it's ancestors
const allEnteringStates = [toState, ...toState.ancestors];
for (let i = (allEnteringStates.length - 1); i > -1; i -= 1) {
const state = allEnteringStates[i];
if (isFunction(state.onEnter)) {
state.onEnter.call(this, {
currentState: state.id,
});
}
}
const oldState = this._currentState;
this._currentState = id;
this.emit(EVENTS.TRANSITION_COMPLETE, {
fromState: oldState,
toState: id,
});
}
}
module.exports = StateMachine;
// 播放器状态机示例
const eventSM = new HSM();
eventSM.addState({ id: 'playing' });
eventSM.addState({ id: 'stopped' });
eventSM.addState({ id: 'paused', from: 'playing' });
eventSM.setInitialState('stopped');
// ...
前端生态上的状态机管理工具
使用 stately.ai/viz 可视化 xstate 定义的状态机。xstate 支持 FSM、HSM、PSM(并行)等,是目前最流行的状态机管理工具
老牌的状态机管理工具,还有 ifandelse/machina.js、matthewp/robot 等,但项目迭代和社区都不是很活跃
正则表达式的状态机可视化(ip 地址识别正则表达式的轨道图可视化)
PS:感兴趣的同学,可以尝试做两个课后作业: