阅读 968

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

写在前面

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

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

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

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

有限状态机

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

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

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

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

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

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

  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的实现以及和自动机的关系(虽然不知道这个业务迭代周期内有没有时间写了。。。)

关注下面的标签,发现更多相似文章
评论