JavaScript与有限状态机

作者: 阮一峰

日期: 2013年9月 2日

有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。

简单说,它有三个特征:

  * 状态总数(state)是有限的。
  * 任一时刻,只处在一种状态之中。
  * 某种条件下,会从一种状态转变(transition)到另一种状态。

它对JavaScript的意义在于,很多对象可以写成有限状态机。

举例来说,网页上有一个菜单元素。鼠标悬停的时候,菜单显示;鼠标移开的时候,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。

代码可以写成下面这样:


  var menu = {
      
    // 当前状态
    currentState: 'hide',
  
    // 绑定事件
    initialize: function() {
      var self = this;
      self.on("hover", self.transition);
    },
  
    // 状态转换
    transition: function(event){
      switch(this.currentState) {
        case "hide":
          this.currentState = 'show';
          doSomething();
          break;
        case "show":
          this.currentState = 'hide';
          doSomething();
          break;
        default:
          console.log('Invalid State!');
          break;
      }
    }
  
  };
  

可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。

另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。

下面介绍一个有限状态机的函数库Javascript Finite State Machine。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱。

该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例。


  var fsm = StateMachine.create();
  

生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述:


  var fsm = StateMachine.create({
  
    initial: 'green',
  
    events: [
      { name: 'warn',  from: 'green',  to: 'yellow' },
      { name: 'stop', from: 'yellow', to: 'red' },
      { name: 'ready',  from: 'red',    to: 'yellow' },
      { name: 'go', from: 'yellow', to: 'green' }
    ]
  
  });
  

交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等。

生成实例以后,就可以随时查询当前状态。

* fsm.current :返回当前状态。
* fsm.is(s) :返回一个布尔值,表示状态s是否为当前状态。
* fsm.can(e) :返回一个布尔值,表示事件e是否能在当前状态触发。
* fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发。

Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例:

* onbeforewarn:在warn事件发生之前触发。
* onafterwarn(可简写成onwarn) :在warn事件发生之后触发。

同时,它也允许为每个状态指定两个回调函数,以green状态为例:

* onleavegreen :在离开green状态时触发。
* onentergreen(可简写成ongreen) :在进入green状态时触发。

假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbeforewarn → onleavegreen → onenteryellow → onafterwarn

除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数。

* onbeforeevent :任一事件发生之前触发。
* onleavestate :离开任一状态时触发。
* onenterstate :进入任一状态时触发。
* onafterevent :任一事件结束后触发。

如果事件的回调函数里面有异步操作(比如与服务器进行Ajax通信),这时我们可能希望等到异步操作结束,再发生状态改变。这就要用到transition方法。


  fsm.onleavegreen = function(){
    light.fadeOut('slow', function() {
      fsm.transition();
    });
    return StateMachine.ASYNC;
  };
  

上面代码的回调函数里面,有一个异步操作(light.fadeOut)。如果不希望状态立即改变,就要让回调函数返回StateMachine.ASYNC,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。

Javascript Finite State Machine还允许指定错误处理函数,当发生了当前状态不可能发生的事件时自动触发。


  var fsm = StateMachine.create({
    // ...
    error: function(eventName, from, to, args, errorCode, errorMessage) {
      return 'event ' + eventName + ': ' + errorMessage;
    },
    // ... 
  });
  

比如,当前状态是green,理论上这时只可能发生warn事件。要是这时发生了stop事件,就会触发上面的错误处理函数。

Javascript Finite State Machine的基本用法就是上面这些,更详细的介绍可以参见它的主页

(完)

留言(23条)

目测就是REST思想的页内运用嗦!

典型的state design pattern 用的不错

我的一些想法
按照有限状态机的定义,它应该包含5个元素:
1起始状态,2结束状态集(可为空),3转移函数,4输入字母表,5状态集

对于键盘鼠标的每一个事件都可以成为输入字母表的一个成员,比如A,B,C……。而视图的各个状态就定义为状态集中的各个成员。

一系列的事件发生,对应一个字符序列,有限状态机逐个吃进字符,根据转移函数完成状态的转换。

完成可以设计一个简单的语言,用于描述有有限状态机,然后用一个js解释器自动完成实际的工作。

终于有一次跟上一峰哥的步伐了,最近也在研究应用js的fsm。
这个库也是我搜索到的感觉很不错的库,最后我也是选择了他。
缺点就是onleavegreen可以简写成ongreen很容易跟onwarn混淆。
不想去修改底层实现,所以在命名在都在事件前加do在状态前加state
就是do_warn, do_go, state_green state_yellow这样。
callbacks里就是ondo_warn ondo_go onstate_green这样。
代码上好阅读很多。

最后的链接写错了,多了一个括号~

@春寒料峭:

谢谢指出,已经更正了。

我觉得第一句话说有限状态机『可以模拟世界上大部分事物』感觉有待商榷。

严格地说有限状态机只能模拟世界上『小部分』事物吧。

对异步操作那个例子有个小疑问,烦请指教一下。

您写到“如果不希望状态立即改变,就要让回调函数返回一个StateMachine.ASYNC对象,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。”

代码中是fsm.onwarn,也就是onleavewarn吧,这个时候状态不是已经改变了么?是不是应该是fsm.onleavegreen?

@子不语:

谢谢指出,你说的完全正确。我压根没注意这个问题,已经改过来了。

以后不用客气,我也是边学边用,希望跟大家一起交流。

引用blabla的发言:

完成可以设计一个简单的语言,用于描述有有限状态机,然后用一个js解释器自动完成实际的工作。

正则表达式跟有限状态机是等价的。

异步回调的问题是打断了正常的逻辑流,函数因为一次IO被迫分成上半部和下半部,但是放在状态机的两个状态里就可以改善这个问题吗?

写过js之后才发现js的表达能力如此给力。

峰哥的博客总是这么漂亮大方,赏心悦目。我想请教一下,你在博客中的嵌入的代码是采用什么插件,还是自己制定的格式?非常感谢!

第一次见js有限状态机在页面控件上的应用是在基于有限状态机的交互组件设计与实现- 前端技术| TaoBaoUED , 貌似现在打不开了.

手工实现的 , 比较累,要是用fsm这个框架应该清爽多了

很强大,学习一下

引用赵英伍的发言:

第一次见js有限状态机在页面控件上的应用是在基于有限状态机的交互组件设计与实现- 前端技术| TaoBaoUED , 貌似现在打不开了.

手工实现的 , 比较累,要是用fsm这个框架应该清爽多了

打不开了 => https://github.com/yhanwen/fsm (翻牆)

喜欢你的博客,语意简洁明了,洗后要多向你学习

周易的演化是不是可以参考这个呢?非常好的三个特征

有限状态机在计算机科学里《计算理论》的重要内容,再演化成图灵机,再接着就有了计算机。很多事情追根溯源就能够找到解决问题的最简单的方法。学习了!

有限状态机没有什么难度,关键在语法分析后自动生成自动机

引用sokoban的发言:

我觉得第一句话说有限状态机『可以模拟世界上大部分事物』感觉有待商榷。

严格地说有限状态机只能模拟世界上『小部分』事物吧。

你说的对, 在真实世界中事务都处于量子叠加态, 不符合: 任一时刻,只处在一种状态之中。

 // 当前状态
 currentState: 'hide',

状态的关键字应用使用 shown/hidden,或者visible/invisible。
hide和show是动词,不应该用于表示状态的地方

阮老师,对于同一时刻有“跑”和“攻击”时候不能触发“攻击2”这种场景能用这个库来处理么

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接