背景介绍
有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机, 是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在计算机科学中, 有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。
有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。它是一个数学模型。是一个抽象机器,在任何时候都可以处于有限数量的状态之一。响应某些外部输入, FSM 可以从一个状态转换到另一个状态; 从一种状态到另一种状态的变化称为过渡。 状态机的行为可以在现代社会中的许多设备中观察到,这些设备根据它们呈现的事件序列执行预定的一系列行为。 例如自动售货机,当存放适当的硬币组合时分配产品,当车辆等待时改变顺序的交通灯等。
在我们前端开发中,我们可以套用有限状态机模型,将业务流程状态化,划分状态和相应的触发事件与动作,利用生命周期事件进行控制与执行
知识剖析
我们来看一下阮一峰大神对有限状态机的描述
它有三个特征:
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(transition)到另一种状态。
有限状态机一般都有以下特点:
- 可以用状态来描述事物,并且任一时刻,事物总是处于一种状态;
- 事物拥有的状态总数是有限的;
- 通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态;
- 事物状态变化是有规则的,A 状态可以变换到 B,B 可以变换到 C,A 却不一定能变换到 C;
- 同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。
状态机综述
关于状态机的一个极度确切的描述是:它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
包含一组状态集(states)、一个起始状态(start state)、一组输入符号集(alphabet)、一个映射输入符号和当前状态到下一状态的转换函数(transition function)的计算模型。当输入符号串,模型随即进入起始状态。它要改变到新的状态,依赖于转换函数。在有限状态机中,会有有许多变量,例如,状态 机有很多与动作(actions)转换(Mealy机)或状态(摩尔机)关联的动作,多重起始状态,基于没有输入符号的转换,或者指定符号和状态(非定有 限状态机)的多个转换,指派给接收状态(识别者)的一个或多个状态,等等。
传统应用程序的控制流程基本是顺序的:遵循事先设定的逻辑,从头到尾地执行。很少有事件能改变标准执行流程;而且这些事件主要涉及异常情况。“命令行实用程序”是这种传统应用程序的典型例子。
另一类应用程序由外部发生的事件来驱动——换言之,事件在应用程序之外生成,无法由应用程序或程序员来控制。具体需要执行的代码取决于接收到的事件,或者它相对于其他事件的抵达时间。所以,控制流程既不能是顺序的,也不能是事先设定好的,因为它要依赖于外部事件。事件驱动的GUI应用程序是这种应用程序的典 型例子,它们由命令和选择(也就是用户造成的事件)来驱动。
Web应用程序由提交的表单和用户请求的网页来驱动,它们也可划归到上述类别。但是,GUI应用程序对于接收到的事件仍有一定程度的控制,因为这些事件要依赖于向用户显示的窗口和控件,而窗口和控件是由程序员控制的。Web应用 程序则不然,因为一旦用户采取不在预料之中的操作(比如使用浏览器的历史记录、手工输入链接以及模拟一次表单提交等等),就很容易打乱设计好的应用程序逻辑。
显然,必须采取不同的技术来处理这些情况。它能处理任何顺序的事件,并能提供有意义的响应——即使这些事件发生的顺序和预计的不同。有限状态机正是为了满足这方面的要求而设计的。
有限状态机是一种概念性机器,它能采取某种操作来响应一个外部事件。具体采取的操作不仅能取决于接收到的事件,还能取决于各个事件的相对发生顺序。之所以能做到这一点,是因为机器能跟踪一个内部状态,它会在收到事件后进行更新。为一个事件而响应的行动不仅取决于事件本身,还取决于机器的内部状态。另外,采取 的行动还会决定并更新机器的状态。这样一来,任何逻辑都可建模成一系列事件/状态组合。
状态机可归纳为4个要素,即现态、条件、动作、次态。这样的归纳,主要是出于对状态机的内在因果关系的考虑。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:
- 现态:是指当前所处的状态。
- 条件:又称为“事件”,当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
- 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
- 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。
示例
一段简单的代码,见识下状态机的威力,使用状态机对 HTML 文档进行词法分析
语言编译的首个步骤,对语言进行分词,最小有意义的单元
const EOF = void 0;
// html词法分析器, 接受一个语法解析器
function HTMLLexicalParser(syntaxer) {
// 状态
let state = data;
// 存储
let token = null;
let attribute = null;
// 接收输入
this.receiveInput = function (char) {
if (state == null) {
throw new Error("there is an error");
} else {
// 更新状态
state = state(char);
}
};
this.reset = function () {
state = data;
};
// 初始状态判断
function data(c) {
if (c === "<") {
return tagOpen; // 标签开始
}
emitToken(c);
return data;
}
// 标签开始状态
function tagOpen(c) {
// 标签结束
if (c === "/") {
return endTagOpen;
}
// 是否为任意大小写字母
if (/[a-zA-Z]/.test(c)) {
token = new StartTagToken();
token.name = c.toLowerCase();
return tagName;
}
return error(c);
}
// 标签名状态
function tagName(c) {
if (c === "/") {
return selfClosingTag;
}
if (/[\t \f\n]/.test(c)) {
return beforeAttributeName;
}
if (c === ">") {
emitToken(token);
return data;
}
if (/[a-zA-Z]/.test(c)) {
token.name += c.toLowerCase();
return tagName;
}
}
// 等待属性名
function beforeAttributeName(c) {
if (/[\t \f\n]/.test(c)) {
return beforeAttributeName;
}
if (c === "/") {
return selfClosingTag;
}
if (c === ">") {
emitToken(token);
return data;
}
if (/["'<]/.test(c)) {
return error(c);
}
attribute = new Attribute();
attribute.name = c.toLowerCase();
attribute.value = "";
return attributeName;
}
// 属性名
function attributeName(c) {
if (c === "/") {
token[attribute.name] = attribute.value;
return selfClosingTag;
}
if (c === "=") {
return beforeAttributeValue;
}
if (/[\t \f\n]/.test(c)) {
return beforeAttributeName;
}
attribute.name += c.toLowerCase();
return attributeName;
}
// 等待属性内容
function beforeAttributeValue(c) {
if (c === '"') {
return attributeValueDoubleQuoted;
}
if (c === "'") {
return attributeValueSingleQuoted;
}
if (/\t \f\n/.test(c)) {
return beforeAttributeValue;
}
attribute.value += c;
return attributeValueUnquoted;
}
// 属性内容双引号
function attributeValueDoubleQuoted(c) {
if (c === '"') {
token[attribute.name] = attribute.value;
return beforeAttributeName;
}
attribute.value += c;
return attributeValueDoubleQuoted;
}
// 属性内容单引号
function attributeValueSingleQuoted(c) {
if (c === "'") {
token[attribute.name] = attribute.value;
return beforeAttributeName;
}
attribute.value += c;
return attributeValueSingleQuoted;
}
// 属性内容
function attributeValueUnquoted(c) {
if (/[\t \f\n]/.test(c)) {
token[attribute.name] = attribute.value;
return beforeAttributeName;
}
attribute.value += c;
return attributeValueUnquoted;
}
// 自封闭
function selfClosingTag(c) {
if (c === ">") {
emitToken(token);
endToken = new EndTagToken();
endToken.name = token.name;
emitToken(endToken);
return data;
}
}
// 结束标签打开
function endTagOpen(c) {
if (/[a-zA-Z]/.test(c)) {
token = new EndTagToken();
token.name = c.toLowerCase();
return tagName;
}
if (c === ">") {
return error(c);
}
}
// 发出Token
function emitToken(token) {
syntaxer.receiveInput(token);
}
function error(c) {
console.log(`warn: unexpected char '${c}'`);
}
}
class StartTagToken {}
class EndTagToken {}
class Attribute {}
module.exports = {
HTMLLexicalParser,
StartTagToken,
EndTagToken,
};
我们可以执行看一下效果
const { HTMLLexicalParser } = require("./lexer");
// html 文档片段
const testHTML = `<html maaa=a >
<head>
<title>cool</title>
</head>
<body>
<img src="a" />
</body>
</html>`;
const dummySyntaxer = {
// 接受一个字符
receiveInput: (token) => {
if (typeof token === "string") {
console.log(`${token.replace(/\n/, "\\n").replace(/ /, "<whitespace>")}`);
} else {
console.log(token);
}
},
};
const lexer = new HTMLLexicalParser(dummySyntaxer);
for (let c of testHTML) {
lexer.receiveInput(c);
}