Promise的实现与标准

转载同事写的文章,从promise的标准角度来说明其实现,这样不管在看Q,还是bluebird的时候,都会容易很多。
JS是如何运行的 每当谈起JS的时候,单线程,异步,回调,非阻塞,event loop这些词汇总是会出现。但是JS到底是如何运行的呢,不妨看一看Philip Roberts在JSConf上讲解event loop的视频。Philip Roberts还自己动手做了一个JS runtime的可视化程序,这个可视化程序他在演讲中也有展示。
让我们来看一段代码吧

console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); process.nextTick(function() { console.log('nextTick'); }); setImmediate(function() { console.log('setImmediate'); }); console.log('script end');

可以看到这段代码中用到了setTimeoutprocess.nextTicksetImmediate,这三个方法在JS中都是异步去执行的,输出结果如下
script start script end nextTick setTimeout setImmediate

为什么同样是异步方法process.nextTick中的回调函数就会在setTimeoutsetImmediate中的回调函数之前执行?这就和task和microtask的机制有关了。关于task和microtask这里有一篇文章(Tasks, microtasks, queues and schedules)讲述的非常好,可以观摩一下,学习学习。
为了更好的理解JS是如何运行的,可以看下图。

Promise的实现与标准
文章图片
image.png 在JS运行的过程中,event loop每一次循环都会将一个task从Task Queue中取出,task执行的过程中会调用不同的函数并压栈。栈中的代码会调用一些API,当这些API执行结束后会将完成的任务加入Task Queue(具体的实现因API而异,有些API可能会在单独的线程中去处理这些操作)。当stack中的代码全部执行完成时会再次从Task Queue中取出一个新的任务来执行,这样就开始了新的一轮loop。
那么Microtask是在什么时候执行的呢?JS会在每一轮loop结束,也就是stack中的代码全部执行完毕时,去执行Microtask Queue中的任务。当Microtask Queue中的任务全部执行完成后再从Task Queue中取出下一个任务。可以理解为执行过程为
Task1 -> Microtask ->Task2
以Task方式运行的有setTimeOutsetImmediate,而已MicroTask方式运行的有process.nextTickMutationObserver。这也就是上面的例子中process.nextTick中回调优先执行的原因。因为process.nextTick中回调被添加到了Microtask Queue,而setTimeOutsetImmediate中的回调则被添加到了Task Queue的末尾,他们在之后的几轮loop中才会被执行。
这里需要提一下有些地方将Task称为MacroTask,将MicroTask称为Jobs。
而在一些具体的实现中,可能会存在多个Task Queue,根据不同的实现目的不同的Task Queue之间存在不同的优先级(例如有些浏览器可能更加注重UI渲染的性能,所以将UI相关任务的Task Queue优先级提高)。
猜测Promise的实现 熟悉Promise的人都知道Promise有三个状态,pendingreslovedrejected。一旦Promise的状态发生改变就再也不会变动,且Promise包含的值也不会被改变。
//e.g.1 console.log('script start'); let promise = new Promise(function(resolve, reject) { console.log('in promise'); resolve('reslove promise'); }); promise.then(function(value) { console.log('resolve: ', value); }, function(reason) { console.log('reason: ', reason); }); console.log('script end')

上面这段代码对于经常使用Promise的人再简单不过了,可以看下他的输出结果。
script start in promise script end resolve:reslove promise

e.g.1的输出结果可以看到传给then方法的回调是在最后执行的,所以可以判断出new Promise(function)中的function是同步执行的,而then(reslove,reject)中的resolve或reject是异步执行的。
熟悉Promise的人对下面一段代码也自然不会感到陌生。
//e.g.2 promise.then((value) => { //do some stuff }).then((value) => { //do some stuff }).then((value) => { //do some stuff }).catch((reason) => { //do some stuff });

为什么Promise能写成链式的,在.then之后还能接着.then?基于这一点可以判断出then方法return的是一个Promise,那么既然是Promise就一定会有状态,那么调用then之后return的这个Promise的状态是如何确定的呢?接着看下面的栗子。
//e.g.3 let promise1 = new Promise(function(resolve, reject) { resolve('reslove promise'); }); let promise2 = promise1.then(function onReslove(value) { console.log('1 resolve: ', value); return 1; }, function onReject(reason) { console.log('1 reason: ', reason); }); promise2.then(function onReslove(value) { console.log('2 resolve: ', value); }, function onReject(reason) { console.log('2 reason: ', reason); }); 执行结果: 1 resolve:reslove promise 2 resolve:1

可以看到当在onReslove中返回一个基础类型的时候promise2的状态变成了resolved
如果把上面的return 1; 改为throw new Error('error'); 会是什么样呢?输出结果如下:
1 resolve:reslove promise 2 reason:Error: error at onReslove (/Users/lx/Documents/projects/VsTest/PromiseExample.js:40:11) at at process._tickCallback (internal/process/next_tick.js:188:7) at Function.Module.runMain (module.js:607:11) at startup (bootstrap_node.js:158:16) at bootstrap_node.js:575:3

可以看到此时promise2的状态变为了rejected。那么如果我在onResolve()中return一个处于不同状态Promise会怎么样呢?
//e.g.4 let promise1 = new Promise(function(resolve, reject) { resolve('reslove promise'); }); let promise2 = promise1.then(function onReslove(value) { console.log('1 resolve: ', value); return promiseReturn; }, function onReject(reason) { console.log('1 reason: ', reason); }); promise2.then(function onReslove(value) { console.log('2 resolve: ', value); return 1; }, function onReject(reason) { console.log('2 reason: ', reason); }); //依次使promiseReturn等于以下值://pending状态的Promise,5s后变为resolved状态 let promiseReturn = new Promise(function(reslove, reject) { setTimeout(() => { reslove(1) }, 5000); }); //输出结果为: 1 resolve:reslove promise //5s之后 2 resolve:1//resolved状态的Promise let promiseReturn = Promise.resolve(1); //输出结果为: 1 resolve:reslove promise 2 resolve:1//rejected状态的Promise let promiseReturn = Promise.reject(new Error('error')); //输出结果为: 1 resolve:reslove promise 2 reason:Error: error at Object. (/Users/lx/Documents/projects/VsTest/PromiseExample.js:33:36) at Module._compile (module.js:569:30)

通过上面的例子可以看到,当onResolve()return一个Promise时,promise2的状态是和return的Promise的状态相同的。
PromiseA+标准 [图片上传失败...(image-a86d48-1516949283239)]
ES标准中的Promise,Q以及bluebird都是PromiseA+标准的实现。 PromiseA+标准主要从三部分提出了对Promise实现的要求,第一部分规定了Promise的状态已经状态的变化。第二部分则指定Promise的then方法的行为。第三部分则是说明了如何决定then方法返回的Promise的状态,并且支持了不同PromiseA+标准实现的Promise之间的兼容性。
PromiseA+标准如下(更具体的标准戳这里):
Promise的状态
Promise必须处于pending,resolved,rejected三个状态之一
  • 当Promise处于pending状态时可以转换到resolvedrejected状态
  • 当Promise处于resolved状态时无法再转换到其他状态,并且有一个无法改变value
  • 当Promise处于rejected状态时无法再转换到其他状态,并且有一个无法改变的reason(reason一般为一个Error对象)
Promise的then方法
Promise的then方法接受两个参数
promise.then(onResolved, onRejected);

  • onResolvedonRejected参数都是可选的,如果onResolvedonRejected不是function,则忽略相应的参数。onResolvedonRejected都不能被调用超过一次。
  • onResolvedonRejected需要通过异步的方式执行,可以用“macro-task”或“micro-task”机制来执行。
  • 同一个Promise的then方法可以被调用多次,当该Promise状态变为resolvedrejected状态时,注册在该Promise上的回调应该根据注册的顺序被调用。
  • then方法会返回一个Promise
    promise2 = promise1.then(onResolved, onRejected);

    1. 如果onResolvedonRejected返回一个x,那么promise2的状态需要根据x来决定(至于如何决定promise2的状态,会在第三部分中说明)。
    2. 如果onResolvedonRejected抛出一个异常e,那么promise2必须rejected且reason = e
    3. 如果promise1是resolved状态且onResolved不是一个function那么promise2必须resolved,并且promise2的value必须与promise1相同
    4. 如果promise1是rejected状态且onRejected不是一个function那么promise2必须rejected,并且promise2的reason必须与promise1相同
The Promise Resolution Procedure
个人感觉这个标题不好“生翻”,直面的翻译可能反倒容易让人误解。可以把这个部分理解为一种操作,该操作需要接受两个参数(promise, x),会根据x的情况来决定promise的状态。
在我们的onResolved回调中一般会return一个value(如果没有写return xxx,那么value就等于undefined)。这里就可以把x当做这个value。调用then方法时返回的Promise的状态就是由这个x来决定的。
如果x是一个thenable(带有then方法的对象或function),那么可以假设x和Promise的行为相似。这一点是为了让不同PromiseA+标准的实现可以兼容。
The Promise Resolution Procedure这个操作的步骤如下:
  • 1.如果xpromise是同一个对象的引用(x === promise),那么reject promise并将一个TypeError赋值给reason
  • 2.如果x是一个Promise(x instanceof Promise),那么promise的状态入下:
    • 2.1 如果x处于pending状态那么promise也处于pending状态,直到x状态变为resolved或rejected。
    • 2.2 如果x处于resolved状态,那么用x的value来resolve promise
    • 2.3 如果x处于rejected状态,那么用x的reason来reject promise
  • 3.如果x是一个对象或function
    • 3.1 如果获取属性x.then的过程中抛出异常e,那么将e作为reason来reject promise
    • 【Promise的实现与标准】3.2 如果x.then是一个function,那么调用x.then传入参数resolvePromiserejectPromise
      • 3.2.1 如果resolvePromise被调用且传入的参数为y,那么再次执行此操作,参数为(promise, y)
      • 3.2.2 如果rejectPromise被调用且传入的参数r,那么将r作为reason来reject promise
      • 3.2.3 如果resolvePromiserejectPromise同时被调用,或者被调用多次,那么优先处理第一次调用,之后的调用都应该被忽略。
      • 3.2.4 如果调用x.then抛出了异常e,若在抛出异常前resolvePromiserejectPromise已经被调用,那么忽略异常即可。若resolvePromiserejectPromise没有被调用过,那么将e作为reason来reject promise
    • 3.3 如果x.then不是一个function,那么用x来resolve promise
  • 4.如果x既不是对象也不是function,那么用x来resolve promise

    推荐阅读