状态迁移库Javascript-state-machine源码解读

2,800 阅读5分钟

背景

前些天在读同事代码的时候,看到同事用了JavaScript-state-machine的库,用来解决在不同状态下执行对应的状态方法。发现通过该库后可以避免在有大量状态的情况下使用过多的if-else/switch等,增强代码的可读性且逻辑高可梳理。借着好奇去读了javascript-state-machine的源码了解大概的实现方式。

相对if-else/switch的优势

通过读该库的示例,可得知该库针对的是状态转移,不同状态之间的切换会触发不同的方法,如下图。

当materialState在进行图中的状态转移时,在使用JavaScript-state-machine库的情况下只需要通过以下方法即可实现

  • JavaScript-state-machine库实现方法,fsm为JavaScript-state-machine库抛出来的StateMachine所创建的实例
fsm.melt(),
fsm.freeze(),
fsm.vaporize(),
fsm.condense(),
fsm.sublimate(),
fsm.deposit(),
  • 常用的if/else or switch的结构都是对于一个变量进行不同状态的判断,而如果需要根据一个状态的前后状态做特定的处理,就相对来说比较繁琐了。
function materialStateChange(prevMaterialState, nextMaterialState){
  if(prevMaterialState == 'solid'){
    if(nextMaterialState == 'liquid'){
      TODO melt();
    }
    if(nextMaterialState == 'gas'){
      TODO sublimate();
    }
  }
  if(prevMaterialState == 'liquid'){
    if(nextMaterialState == 'solid'){
      TODO freeze();
    }
    if(nextMaterialState == 'gas'){
      TODO vaporize();
    }
  }
  if(prevMaterialState == 'gas'){
    if(nextMaterialState == 'liquid'){
      TODO condense();
    }
    if(nextMaterialState == 'solid'){
      TODO deposit();
    }
  }
}

针对以上二种处理方式,很明显第一种处理方式会更简介且可好的体现单一性原则。同时如果在有更多状态的情况下第二种的处理方式会更加的复杂。

Javascript-state-machine库的大致用法

  • 根据实际情况画出状态转移图
  • 声明StateMachine实例且传递状态机的参数options
    • init:状态初始值
    • transitions:状态转移过程
    • methods:状态转移后的钩子
    • data:实例内的数据
  • 根据情况进行状态间的转换 只需要执行transitions中相应的name方法
    • 如需要从liquid执行到gas,只需执行fsm.vaporize()即可
    • 而onVaporize方法则在materialState从liquid转变为gas后触发执行

Javascript-state-machine库的大致实现

  • 主要代码
function StateMachine(options) {
  return apply(this || {}, options);
}

function apply(instance, options) {
  var config = new Config(options, StateMachine);//将声明实例时传的配置选项options传入Config中,config会对传入的配置选项以及自身的方法构建出一个状态机实例所需要各个属性
  build(instance, config);
  instance._fsm();//执行build方法中声明的_fsm方法
  return instance;//返回instance以供StateMachine创建实例
}
function Config(options, StateMachine) {
  options = options || {};
  this.options     = options; // preserving original options can be useful (e.g visualize plugin)
  this.defaults    = StateMachine.defaults;
  this.states      = [];
  this.transitions = [];
  this.map         = {};
  this.lifecycle   = this.configureLifecycle();
  this.init        = this.configureInitTransition(options.init);//配置init的transition
  this.data        = this.configureData(options.data);处理后的this.data为functionreturn传入的data对象
  this.methods     = this.configureMethods(options.methods);//this.methods为option.methods || {}
  this.map[this.defaults.wildcard] = {};
  this.configureTransitions(options.transitions || []);//将option.transitions数组的各个项存入this.maps中具体在mapTransition中
  this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin);//可提供state-machine-history 插件查看状态的历史值,具体使用链接https://github.com/jakesgordon/javascript-state-machine/blob/0d603577423244228cebcd62e60dbbfff27c6ea3/docs/state-history.md
}
function build(target, config) {
  if ((typeof target !== 'object') || Array.isArray(target))
    throw Error('StateMachine can only be applied to objects');
  plugin.build(target, config);
  Object.defineProperties(target, PublicProperties);
  mixin(target, PublicMethods);
  mixin(target, config.methods);
  config.allTransitions().forEach(function(transition) {
    target[camelize(transition)] = function() {
      return this._fsm.fire(transition, [].slice.call(arguments))
    }
  });//扩充target属性:对transitions的所有transition转换为驼峰后赋值一个方法,该方法的具体作用返回下面说 //todo
  target._fsm = function() {
    this._fsm = new JSM(this, config);//实例化了一个真真正正的状态机JSM
    this._fsm.init(arguments);//状态机初始化
  }
}

JSM文件 略去非主流程代码

function JSM(context, config) {
  this.context   = context;
  this.config    = config;
  this.state     = config.init.from;
  this.observers = [context];
}
mixin(JSM.prototype, {
  init: function(args) {
    if (this.config.init.active)
      return this.fire(this.config.init.name, []);
  },

  seek: function(transition, args) {  },//返回this.map[this.state][transition].to,当前状态下将要转变的行为todo

  fire: function(transition, args) {
    return this.transit(transition, this.state, this.seek(transition, args), args);
  },

  transit: function(transition, from, to, args) {
    var lifecycle = this.config.lifecycle,
        changed   = this.config.options.observeUnchangedState || (from !== to);

    if (!to)
      return this.context.onInvalidTransition(transition, from, to);

    if (this.isPending())
      return this.context.onPendingTransition(transition, from, to);

    this.config.addState(to);  

    this.beginTransit();

    args.unshift({             // this context will be passed to each lifecycle event observer
      transition: transition,
      from:       from,
      to:         to,
      fsm:        this.context
    });

    return this.observeEvents([
                this.observersForEvent(lifecycle.onBefore.transition),
                this.observersForEvent(lifecycle.onBefore[transition]),
      changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED,
      changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED,
                this.observersForEvent(lifecycle.on.transition),
      changed ? [ 'doTransit', [ this ] ]                       : UNOBSERVED,
      changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED,
      changed ? this.observersForEvent(lifecycle.onEnter[to])   : UNOBSERVED,
      changed ? this.observersForEvent(lifecycle.on[to])        : UNOBSERVED,
                this.observersForEvent(lifecycle.onAfter.transition),
                this.observersForEvent(lifecycle.onAfter[transition]),
                this.observersForEvent(lifecycle.on[transition])
    ], args);
  },
  
  doTransit:    function(lifecycle) { this.state = lifecycle.to;},

module.exports = JSM;

this.observersForEvent(transitionName)所返回的是[transitionName, result, true],而result则会将注册transitionName的状态机实例作为数组返回,
在transit方法的返回值中,this.observeEvents的第一个参数包括了一系列值,前五个this.observersForEvent代表状态逐渐失效的过程、后六个this.observersForEvent则代表逐渐转变为新状态与行状态转换之后的过程
而中间的 [ 'doTransit', [ this ] ] 则会将to的值更新为当前状态,当执行完doTransit之后,便代表进入了当前状态机新进入了新的的状态,就会执行一系列的钩子函数
而钩子函数的内容是抛出来给开发者进行编写业务代码的,所以我们可以在onWarn等状态转移的函数进行业务代码的编写或者更新状态机的data值等操作

总结

总的来说该状态机的大致流程是 1.根据transitions形成map对象,该对象会存储状态机下所有的状态,该状态的内容是该状态下所能触发的行为,如下图

2.当执行this.melt方法时,会执行已经在build方法中已为实例声明的方法,所以this.melt()执行的是this._fsm.fire(transition, [].slice.call(arguments))语句

function build(target, config) {
 //其余代码已省略
  config.allTransitions().forEach(function(transition) {
    target[camelize(transition)] = function() {
      return this._fsm.fire(transition, [].slice.call(arguments))
    }
  });
}

3.在fire方法中会通过seek方法获取到将要转换的值,即to,在this.melt()执行的时候to为liquid,再将transition(行为名字)、this.state、to传给transit, 4.在transit中内部则主要执行observeEvents方法,参数分为三部分,第一部分代表在状态更新前的的一系列钩子函数处理,第二部分通过doTransit方法对状态进行更新,第三部分则为状态转换之后的钩子函数处理。

5.observeEvents方法的主要代码部分

var event     = events[0][0],
        observers = events[0][1],
        pluggable = events[0][2];
if (observers.length === 0) { // 判断当前生命周期是否有被状态机注册过,如果未注册则observers.length === 0,shift当前hook之后继续执行observeEvents
  events.shift();
  return this.observeEvents(events, args, event, previousResult);
}
else {
  var observer = observers.shift(),// 如果状态机被注册过则行对应的hook方法
      result = observer[event].apply(observer, args);
  if (result && typeof result.then === 'function') { // 如果注册的methods返回的为promise,则进行异步处理
    return result.then(this.observeEvents.bind(this, events, args, event))
                 .catch(this.failTransit.bind(this))
  }
  else if (result === false) {
    return this.endTransit(false);
  }
  else {
    return this.observeEvents(events, args, event, result);
  }
}

6.以上便是Javascript-state-machine库的大致流程,如有不足多多指教。如果存在较多的判断条件的场景,相比于if-else/switch的写法使用状态迁移库可能会更适合你