转载同事写的文章,从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');
可以看到这段代码中用到了setTimeout
、process.nextTick
和setImmediate
,这三个方法在JS中都是异步去执行的,输出结果如下
script start
script end
nextTick
setTimeout
setImmediate
为什么同样是异步方法process.nextTick
中的回调函数就会在setTimeout
和setImmediate
中的回调函数之前执行?这就和task和microtask的机制有关了。关于task和microtask这里有一篇文章(Tasks, microtasks, queues and schedules)讲述的非常好,可以观摩一下,学习学习。
为了更好的理解JS是如何运行的,可以看下图。
在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方式运行的有setTimeOut
、setImmediate
,而已MicroTask方式运行的有process.nextTick
、MutationObserver
。这也就是上面的例子中process.nextTick
中回调优先执行的原因。因为process.nextTick
中回调被添加到了Microtask Queue
,而setTimeOut
和setImmediate
中的回调则被添加到了Task Queue
的末尾,他们在之后的几轮loop中才会被执行。
这里需要提一下有些地方将Task称为MacroTask,将MicroTask称为Jobs。
而在一些具体的实现中,可能会存在多个Task Queue
,根据不同的实现目的不同的Task Queue
之间存在不同的优先级(例如有些浏览器可能更加注重UI渲染的性能,所以将UI相关任务的Task Queue优先级提高)。
猜测Promise的实现
熟悉Promise的人都知道Promise有三个状态,pending
、resloved
和rejected
。一旦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 <anonymous>
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.<anonymous> (/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
状态时可以转换到resolved
或rejected
状态 - 当Promise处于
resolved
状态时无法再转换到其他状态,并且有一个无法改变value
- 当Promise处于
rejected
状态时无法再转换到其他状态,并且有一个无法改变的reason
(reason一般为一个Error对象)
Promise的then方法
Promise的then方法接受两个参数
promise.then(onResolved, onRejected);
onResolved
和onRejected
参数都是可选的,如果onResolved
或onRejected
不是function,则忽略相应的参数。onResolved
和onRejected
都不能被调用超过一次。onResolved
和onRejected
需要通过异步的方式执行,可以用“macro-task”或“micro-task”机制来执行。同一个Promise的
then
方法可以被调用多次,当该Promise状态变为resolved
或rejected
状态时,注册在该Promise上的回调应该根据注册的顺序被调用。-
then
方法会返回一个Promisepromise2 = promise1.then(onResolved, onRejected);
- 如果
onResolved
或onRejected
返回一个x
,那么promise2
的状态需要根据x
来决定(至于如何决定promise2
的状态,会在第三部分中说明)。 - 如果
onResolved
或onRejected
抛出一个异常e
,那么promise2
必须rejected且reason = e
。 - 如果
promise1
是resolved状态且onResolved
不是一个function那么promise2
必须resolved,并且promise2
的value必须与promise1
相同 - 如果
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.如果
x
和promise
是同一个对象的引用(x === promise
),那么rejectpromise
并将一个TypeError
赋值给reason-
2.如果
x
是一个Promise(x instanceof Promise
),那么promise
的状态入下:2.1 如果
x
处于pending状态那么promise
也处于pending状态,直到x
状态变为resolved或rejected。2.2 如果
x
处于resolved状态,那么用x
的value来resolvepromise
。2.3 如果
x
处于rejected状态,那么用x
的reason来rejectpromise
-
3.如果
x
是一个对象或function3.1 如果获取属性
x.then
的过程中抛出异常e
,那么将e
作为reason来rejectpromise
-
3.2 如果
x.then
是一个function,那么调用x.then
传入参数resolvePromise
和rejectPromise
3.2.1 如果
resolvePromise
被调用且传入的参数为y
,那么再次执行此操作,参数为(promise, y)
3.2.2 如果
rejectPromise
被调用且传入的参数r
,那么将r
作为reason来rejectpromise
3.2.3 如果
resolvePromise
和rejectPromise
同时被调用,或者被调用多次,那么优先处理第一次调用,之后的调用都应该被忽略。3.2.4 如果调用
x.then
抛出了异常e
,若在抛出异常前resolvePromise
或rejectPromise
已经被调用,那么忽略异常即可。若resolvePromise
或rejectPromise
没有被调用过,那么将e
作为reason来rejectpromise
3.3 如果
x.then
不是一个function,那么用x
来resolvepromise
4.如果
x
既不是对象也不是function,那么用x
来resolvepromise