从Promise的实现来看有限状态机

写在前面

有限状态机在我读研的时候是一门必修的课程,也就是大部分CS研究生都要接触的一门课程。这门课说简单也蛮简单的,可是其中内含的内容以及应用实在是太多了。javascript

有人说为何这么简单的一个东西要用看起来很复杂的数学模型来表示呢?由于咱们平时接触到的不少编程相关的知识都是从这个数学模型上发展出来的。可能在你本身不知道的时候,已经使用了这个理论来coding。数学抽象可以让人更加系统的了解这个理论,而且进行推导。前端

干说是无味的。咱们能够从前端的一些东西中看到有限状态机的影子。那么就从一些实际应用开始,来理解下有限状态机。java

这一个系列可能分红好几篇文章,又臭又长,从简单到抽象。git

有限状态机

先来简单描述一下有限状态机,百科上给的解释很简单:github

有限状态机(英语:finite-state machine:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动做等行为的数学模型。----来自wiki编程

有限状态机这个名称的核心是两个词:有限和状态(彷佛说了句废话。。)。promise

有限在这里是定语,表示状态是有限的,也就是一个正确的有限状态机只有有限个状态,可是容许这个状态机不间断地运行下去。浏览器

最简单也最经常使用的有限状态机的例子就是红绿灯,三个颜色就表明了三个不一样的状态,根据必定的条件(这里就是延时),触发状态的转变。bash

构成一个状态机的最简单的几个因素(这里以红绿灯做为例子):框架

  1. 状态集:红、黄、绿;
  2. 初始状态:红绿灯的启动状态;
  3. 状态转移函数:延时或者动态调度;
  4. 最终状态:红绿灯关闭的状态,固然也可能不存在;

说了这么多,都是数学模型。那么在前端领域有没有一个简单的有限状态机呢。固然不少不少,这篇就先来讲一下Promise和状态机的关系。

Promise的实现

直接说Promise和状态机的关系可能没有那么好理解,这里先把结论留下,再看一个简单的Promise的实现代码,就能够大体理解有限状态机在Promise中的功能了。

Promise是一个具备四个状态的有限状态机,其中三个核心状态为PendingFulfilled以及Rejected分别表示该Promise挂起,完成以及拒绝。还有一个额外的初始状态,表示Promise还未执行,这个状态严格上来讲不算是Promise的状态,可是在实际Promise的使用过程当中,都会具有这个状态。

根据上面的阐述,就大概搭建起了这个有限状态机的框架。

那么就能够从有限状态机的构成因素,来本身实现一个Promise了。

Promise

Promise是ES6标准提供的一个异步操做的很好的语法糖,对于本来的回调函数模式进行了封装,实现了对于异步操做的链式调用。而且配上generator以及async语法糖来使用更加方便。

虽然Promise当前在不少浏览器上都已经获得了支持,可是在看Promise的时候,发现对于Promise的不少地方仍然不是很了解。包括其内部的实现机制,写这个代码的目的也是在于对Promise的使用更加了如指掌。

Promise的具体使用方法能够看个人这一篇博客,这里就不对Promise对象自己的使用进行说明了,默认你们都已经掌握基本的Promise的使用方法了。若是不甚了解的话,请看Promisegeneratorasync/await

下面具体的代码能够参见个人github中的fake-promise

初始状态:new Promise

首先,对于ES6原生的Promise对象来讲,在初始化的过程当中,咱们传递的是一个function(resolve, reject){}函数做为参数,而这个函数是用来进行异步操做的。

目前javascript中的大部分异步操做都是使用callback的方式进行的,Promise的回调传入一个函数,这个函数的接受两个参数,分别在状态转变为FULFILLED以及REJECTED的时候被调用。

若是异步操做失败的话,那么天然就是将失败缘由处理以后,调用reject(err)函数。

var p = new Promise(function(resolve, reject) {
  fs.readFile('./readme', function(err, data) {
    if (err) {
      reject(err);
    } else {
      resolve(data);
    }
  });
});
复制代码

也就是这个两个参数函数不管如何,都是会在异步操做完成以后调用的。

那针对这一点,能够先这样写Promise的构造函数(这是Promise-polyfill的大致框架和初始化函数):

const promiseStatusSymbol = Symbol('PromiseStatus');
const promiseValueSymbol = Symbol('PromiseValue');
const STATUS = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
};
const transition = function(status) {
  var self = this;
  return function (value) {
    this[promiseStatusSymbol] = status;
    this[promiseValueSymbol] = value;
  }
}
const FPromise = function(resolver) {
  if (typeof resolver !== 'function') {
    throw new TypeError('parameter 1 must be a function');
  }
  this[promiseStatusSymbol] = STATUS.PENDING;
  this[promiseValueSymbol] = [];
  this.deps = {};
  resolver(
    // 这里返回两个函数,这两个函数也就是resolver和reject。
    // 这两个函数会分别对于当前Promise的状态和值进行修改
    transition.call(this, STATUS.FULFILLED),
    transition.call(this, STATUS.REJECTED)
  );
}
复制代码

在进行了new Promise的初始化以后,这个Promise就进入了本身的第一个状态,也就是初始态。

初始状态

状态集:PENDINGFULFILLEDREJECTED

这里的FULFILLED状态其实就是Resolved,只不过Resolved这个单词太具备迷惑性了,FULFILLED更能体现这个状态的意义。

根据使用Promise的经验,其整个生命周期应该是具备状态的,当开始异步操做,可是尚未结果的时候,应该是挂起状态PENDING,而后是成功和失败的状态。

传入到构造函数中的函数须要在构造函数中被调用,来开始异步操做。而后经过咱们传递进去的两个函数来分别修改为功和失败的状态以及值。

当咱们调用了封装为Promise的函数以后,这个状态机就启动了。启动以后,假设这个异步操做要执行10S,那么状态机在执行以后,会由Start变为PENDING,表示这个异步操做被挂起。

10秒的PENDING状态在执行异步操做完成了以后,存在两个分支:

  1. 若是这个异步操做成功,并未抛出错误,那么状态机跳转到FULFILLED
  2. 若是异步操做失败,或者抛出了错误,那么状态机跳转到REJECTED

resolve & reject

上面的整个过程是Promise状态机的最根本的一个过程,可是Promise是能够进行链式调用的,也就是这个状态机能够循环往复地进行状态的改变。

FPromise.prototype.then = function(onFulfilled, onRejected) {
  const self = this;
  return FPromise(function(resolve, reject) {
    const callback = function() {
      // 注意这里,对于回调函数执行时候的返回值,也须要保存下来,
      // 由于链式调用的时候,这个参数应该传递给链式调用的下一个
      // resolve函数
      const resolveValue = onFulfilled(self[promiseValueSymbol]);
      resolve(resolveValue);
    }
    const errCallback = function() {
      const rejectValue = onRejected(self[promiseValueSymbol]);
      reject(rejectValue);
    }
    // 这里是对当前Promise状态的处理,若是上一个Promise在执行then方法以前就已经
    // 完成了,那么下一个Promise对应的回调应该直接执行
    if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
      return callback();
    } else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
      return errCallback();
    } else if (self[promiseStatusSymbol] === STATUS.PENDING) {
      self.deps.resolver = callback;
      self.deps.rejecter = errCallback;
    }
  })
}
复制代码

then方法应该是Promise进行链式调用的根本。

首先,then方法具备两个参数,分别是成功和失败的回调,

而后,其应该返回一个新的Promise对象来给链式的下一个节点进行调用,

最后,这里若是自己Promise对象的状态已是FULFILLED或者REJECTED了,那么就能够直接调用回调函数了,不然须要等待异步操做的完成状态发生。

状态转移函数:链式调用

严格来讲,每次进行状态的转移都是根据当前异步操做的执行状态来进行判断的。可是每次异步操做的迭代都是依赖Promise的链式操做,不然这个状态机也不会产生如此多的状态转移过程。

依赖收集

链式调用的根本是依赖收集,通常来讲,Promise中的代码都是异步的,在执行函数的不可能当即执行回调内的函数。

if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
	return callback();
} else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
	return errCallback();
} else if (self[promiseStatusSymbol] === STATUS.PENDING) {
	// 通常都是PENDING状态,直接收集回调
	self.deps.resolver = callback;
	self.deps.rejecter = errCallback;
}
复制代码

依赖被收集到一块儿以后,在状态发生变化的时候,咱们采用一个setter来对状态的变化进行响应,而且执行对应的回调。

const transition = function(status) {
  return (value) => {
    this[promiseValueSymbol] = value;
    setStatus.call(this, status);
  }
}
/** 
  * 对于状态的改变进行控制,相似于存取器的效果。
  * 若是状态从 PENDING --> FULFILLED,则调用链式的下一个onFulfilled函数
  * 若是状态从 PENDING --> REJECTED, 则调用链式的下一个onRejected函数
  *
  * @returns void
  */
const setStatus = function(status) {
  this[promiseStatusSymbol] = status;
  if (status === STATUS.FULFILLED) {
    this.deps.resolver && this.deps.resolver();
  } else if (status === STATUS.REJECTED) {
    this.deps.rejecter && this.deps.rejecter();
  }
}
复制代码

当第一个异步执行完毕后,会执行其依赖中的resolver或者rejecter。而后咱们会在这个resolver中返回一个新的Promise,那么这个新的Promisep2就能够接着p1开始执行,p2的结构和p1是如出一辙的,在其被构造了以后,一样地,进行依赖收集以及链式调用,造成了一个状态屡次循环的有限状态机。

完整的状态机

有限状态机与Promise

到了这里,你们应该都能看到有限状态机和Promise之间的关系了。其实Promise除了依赖收集过程以外,就是一个相似红绿灯的有限状态机。

Promise基本上具备一个有限状态机的全部主要因素。一个Promise的状态机在其生命周期中经过状态的转移,来控制异步函数的同步执行,在必定程度上保证了回调函数的callback hell

除了Promise这个比较简单的,采用了有限状态机数学模型的实现以外,前端还有其余和状态机相关的实践。而且还有很是复杂的实践。下一篇会讲一下Redux的实现以及和自动机的关系(虽然不知道这个业务迭代周期内有没有时间写了。。。)

相关文章
相关标签/搜索