你真的知道 promise 是如何运行的吗?很多手写 promise 的筒子们,你们看看这些手写后的代码是否解释下面的案例,悄悄告诉你,很可能不能哟!
先来一道开胃菜,以下内容输出什么?
javascript
new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
}).then(() => {
console.log('resolvePromise resolved')
})
Promise.resolve()
.then(() => { console.log('promise1') })
.then(() => { console.log('promise2') })
.then(() => { console.log('promise3') })
答案:
javascript
promise1
promise2
resolvePromise resolved
promise3
为什么 "resolvePromise resolved" 会在 "promise2" 后输出呢?
好吧,可能是你刚才眼花了,没发挥好, 再来一道试试:
javascript
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
答案:0,1,2,3,4,5,6
是不是有点感觉了。。。。。懵懵哒的。。感觉!
本文最终结论是结合了 Standard ECMA-262 2024版 对 promise 的要求,所以无需怀疑正确性。
从头讲起
为了彻底解决这些问题,我们必须具备 promise 的基础知识!
new Promise的时候,传入的回调函数会有 resolve 和 reject 参数被传入:
javascript
new Promise(function(resolve, reject) {
resolve(5);
});
- Promise 的初始状态是 pending
- 然后内部会有一个叫做 executor 的函数会自动调用(不了解 executor 没关系,不影响理解它的机制)
- 其执行的成功和失败会让 Promise 的 state, 也就是状态发生变化,state 会在 resolve 调用的时候, "fulfilled",完成态 ,在 reject 调用的时候变为 "rejected",失败状态。
在这里,我先消除一些误解,在调用 resolve 的时候,resolve 是一个会根据参数的不同,进行差别极大的后续处理
- 如果 resolve 的值是一个 普通值,如下图,这在ecma 标准里叫做 is not an Object,然后会执行
FullfillPromise方法,传入参数为
- promise,当前的 promise 实例
- resolution,传递给 resolve 的参数
FullfillPromise的执行,我简单描述一下,在 ecamscript 文档里也有,然后 promise 会变为完成态,然后遍历then 方法(所有调用的 then 方法会放到一个队列里,依次被 for 循环调用)。
这里会涉及到非常复杂的 resolve 判断,我依次过一下。
首先,第 8 条之前的规则对我们来说百分之 99.9 不会遇到,就不谈了。
从第 8 条开始,第一个我们知道,只要不传入 Object 就立马变为完成态,例如
javascript
new Promise(function(resolve, reject) {
resolve('hello');
});
// Promise {<fulfilled>: 'hello'}
第 9 条:
Let then be Get(resolution, "then"). If then is an abrupt completion, then return RejectPromise(promise, then.[[Value]]).
如果 resolution 也就是 resolve 的参数是一个对象,并且有 then 属性,如果这个 then 抛出异常,那么会返回 reject 的 promise
javascript
const value = {};
Object.defineProperty(
value,
'then',
{ get() { throw new Error('No "then"!'); } }
);
Promise.resolve(value).catch(
e => console.log(`Error: ${e}`)
);
// log: Error: No "then"!
Promise resolve 参数是一个对象,并且 then 属性的值不是函数,则返回这个对象本身
Let thenAction be then.[[Value]]. If IsCallable(thenAction) is false, then return FulfillPromise(promise, resolution).
javascript
Promise.resolve(
{ then: 42 }
).then(
value => console.log(`Resolution with: ${JSON.stringify(value)}`)
);
// log: Resolution with: { "then": 42 }
Promise resolve 参数是一个对象,并且 then 属性的值是函数,则这个函数会被当做正常传入 promise (带有 resolve 和 reject )的函数调用
javascript
Promise.resolve(
{ then: (...args) => console.log(args) }
).then(value => console.log(`Resolution with: ${value}`));
// log: [fn, fn]
// | \--- reject
// resolve
// !!! 没有触发后面的 then,因为 promise 没有 resolve
好了,有了这个基础之后,我们看下 Promise.resolve() 这个 api 的用法
Promise.resolve() 返回一个完成后的 Promise. 也就是如果给 resolve 传入的是一个 promise ,那么返回 promise,否则返回一个 fulfilled 状态 的promise。
- Promise.resolve(5) -> 返回 fulfilled 状态的 promise
- Promise.resolve(Promise.resolve(5)) -> 返回 fulfilled 状态的 promise
注,这里 Promise.resolve,只是暂且可以理解为传入的值会返回 fulfilled 状态的 promise ,但实际上有些边界条件很棘手,不过平时是完全用不到的(有兴趣的同学可以去研究,比如 Promise.resolve(() => Promise.resolve(5)))。
好了,奇怪的事情马上就要发生了, 先来一个铺垫!
javascript
const promise = new Promise(function(resolve, reject) {
resolve(5);
});
console.log(promise);
上面你猜返回什么,没错,也是 fulfilled 状态的 promise,这个很符合我们的直觉,我们接着看!
下面代码返回什么呢?
javascript
const promise = new Promise(function(resolve, reject) {
resolve(Promise.resolve(5));
});
console.log(promise);
你是不是同样直觉上认为是 fulfilled 状态的 promise 呢?
对不起,错了哦,是 pending 状态的 promise。
我们把上面的问题简单画成图,让大家好好看下奇怪之处!
为什么是这样呢?
直觉看来不可靠啊,我不得不去 ecmascript 262 的英文文档里面寻求答案了,其实执行 promise 有很多过程,但我们关心的是影响我们刚才结果的规则到底在哪,全部规则网址如下,有兴趣的同学自己可以去探索,跟我们之前问题相关的规则是 第13 - 15 条规则,
javascript
13. Let thenJobCallback be HostMakeJobCallback(thenAction).
14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).
15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
16. Return undefined.
是不是也有点懵逼,看不懂没关系,马上来翻译一下:
其中 NewPromiseResolveThenableJob(promise, resolution, thenJobCallback),NewPromiseResolveThenableJob 是抽象的一个概念,由宿主环境自定义,其中参数:
- promise 是指当前执行的 promise
- resolution 是指 resolve()中的参数,也就是 Promise.resolve(5)
- thenJobCallback 是指 Promise.resolve(5) 返回的 then 方法
然后 NewPromiseResolveThenableJob 的返回值 job, 被放到微任务队列里(HostEnqueuePromiseJob(job))
所以说 resolve(Promise.resolve(5)); 中 Promise.resolve(5) 并没有执行,而是 NewPromiseResolveThenableJob 执行返回的微任务放到了 微任务队里。
好了,我回顾一下文章开始的面试题:
javascript
new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
}).then(() => {
console.log('resolvePromise resolved')
})
上面的 resolve(resolvedPromise) 这就是所谓在我们眼里多了的一个微任务,那么为什么后面还有一个多了的微任务,我们接着看。
javascript
.then(() => {
console.log('resolvePromise resolved')
})
这里调用的就是 NewPromiseResolveThenableJob 中生成的 job,如下图:
实际上调用的就是红框部分的代码,我来翻译一下:
HostCallJobCallback(then, thenable, resolvingFunctions[[Resolve]], resolvingFunctions[[Resolve]])
参数,就是我们之前的提到的:
- 第一个参数 then ,实际上就是题里我们说的 Promise.resolve() 的 then 方法
- 第而个参数 thenable,就是当执行的 promise 实例(new Promise)
- 第三个参数就是 promise 实例被传入的 resolve 方法
- 第四个参数就是 promise 实例被传入的 reject 方法
HostCallJobCallback 最终是如何调用的呢,我们接着看:
翻译成 js 代码就是(上图的 Call,就是我们 js 里函数的 call 方法)
javascript
Promise.resolve().then(
promise 实例,
promise 实例被传入的 resolve 方法,
promise 实例被传入的 reject 方法)
所以这里的 then 会创造一个微任务。
我们回到文章开头的两个题,我们来梳理一下:
javascript
new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
}).then(() => {
console.log('resolvePromise resolved')
})
Promise.resolve()
.then(() => { console.log('promise1') })
.then(() => { console.log('promise2') })
.then(() => { console.log('promise3') })
首先 resolve(resolvedPromise) 因为 resolvedPromise 不是 Object,并且包含 then 方法,那么会产生一个微任务,把 Promise.resolve().then() 放入这个方法里,然后放入微任务
接着
javascript
Promise.resolve()
.then(() => { console.log('promise1') })
.then(() => { console.log('promise2') })
.then(() => { console.log('promise3') })
把上面第一个回调 () => { console.log('promise1') } 放入微任务,微任务队列如下:
- 第一个 () => { Promise.resolve().then() }
- 第二个 () => { console.log('promise1') }
然后,Promise.resolve().then() 开始执行,因为有 then,产生第二个微任务,此时的微任务队列如下:
- () => { console.log('promise1') }
- Promise.resolve() 中 then 方法的回调函数
然后打印 promise1,接着,放入
javascript
Promise.resolve()
.then(() => { console.log('promise1') })
.then(() => { console.log('promise2') })
.then(() => { console.log('promise3') })
中第二个 then 的回调函数到微任务队列,此时的微任务队列如下:
- Promise.resolve() 中 then 方法的回调函数
- () => { console.log('promise2') }
接着 Promise.resolve() 中 then 方法的回调函数返回一个 promise,此时把
javascript
new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
}).then(() => {
console.log('resolvePromise resolved')
})
中的 () => { console.log('resolvePromise resolved') } 放入微任务队列,此时的微任务队列如下:
- () => { console.log('promise2') }
- () => { console.log('resolvePromise resolved') }
后面的就很简单了,我就不赘述了,文章开头的第二个题,你来根据我上面的解释,自己尝试解释一下,是不是豁然开朗了?
这是我 Node.js 系列文章的第三篇,也是探索 Javascript 异步任务管理的第一篇,其它几篇链接如下,欢迎点赞,收藏。其实可以出一个 Javascript 介绍核心概念的小册,不知道大家有兴趣没?