这就是编程:V8 Promise是如何承诺我们
前言
2022年,文章写少了,源码也看少了,进入2023年,就希望能坚持写作,把每一次源码阅读的过程都记录下来,最后收到一个系列中,就这样,《这就是编程》出现了。
下面这段话将作为该系列的slogan:
“《这就是编程》,每周六22:00定期更新,让我们如期打开编程世界的大门。借助源码,让我们进入编程宇宙,自由翱翔、尽情探索其中奥秘,让我们一起读懂源码,读懂编程,我相信这其中有着无限乐趣与惊喜。”
然后今天带来关于V8 Promise的源码分析,一文带你全面了解V8 Promise的实现原理及使用细节,让你对js异步编程有新的认识。
Let's start!
初识Promise
JSPromise在cxx里结构是什么:
state:分别为pending、fulfilled、rejected
reactions_or_result的可能值:
- PromiseReactions,简单理解为promise chain链表
- 结果值,即resolve/reject函数调用时传入的参数值
这些属性在js对象上的表达是以下internal slot:
- [[PromiseState]]
- [[PromiseResult]]
实例创建
接下来看在cxx层面promise原理,首从Promise构建器开始。
一旦js层面new Promise(executor)执行,对应cxx源码里PromiseConstructor执行,
- 创建JSPromise实例
- 创建PromiseResolvingFunctions,也就是resolve/reject方法对,它们是两个built-in方法,也是JSPromis实例状态变更的唯一触发点,可谓Promise核心机制之一
- 调用executor(resolve, reject),通过try-catch进行捕获并直接进入rejected状态,所以executor里抛错同样能被onRejected handler处理
- 最后返回JSPromise实例
注意:
在Promise构建过程中executor是同步执行
绑定回调
得到JSPromise实例后,就一定会调用其then方法,不然JSPromise实例就失去了意义。
js层面Promise.prototype.then是一个built-in方法,对应cxx层面的PromisePrototypeThen。
- 获取onFulfilled/onRejected handlers的执行上下文context
- 创建PromiseCapability,一个deferred的对象,既包含JSPromise实例,也包含对应的resolve/reject方法对,即JSPromise实例总是和resolve/reject方法对搭配出现;同时PromiseCapability是promise chain的关键
- 针对刚刚创建的PromiseCapability,加上onFulfilled/onRejected handlers信息,创建PromiseReaction,PromiseReaction是后续handler被调用的载体,因为它是创建PromiseReactionJobTask的依据,然后配合当前JSPromise实例的reactions_or_result建立PromiseReactions链表
- 返回新创建的JSPromise实例
注意:
考虑这样的场景,针对同一个JSPromise实例,多次进行then方法调用,这时PromiseReactions链表里保存会每一次then方法调用的onFulfilled/onRejected handlers信息。
举个例子:
const myPromise4 = new Promise((resolve, reject) => {
setTimeout(_ => {
resolve('my code delay 5000')
}, 5e3)
})
myPromise4.then(result => {
console.log('第 1 个 then')
})
myPromise4.then(result => {
console.log('第 2 个 then')
})
此时myPromise4的reactions_or_result所存放的PromiseReactions链表示意如下图:
为了更加直观的表示,这里以handler代替PromiseReaction实例
针对then方法调用后,handler先绑定先执行,但上图反映的PromiseReactions链表其实顺序刚好相反,先埋个伏笔。
OK,接着看,其实then方法核心逻辑在PerformPromiseThenImpl。
当promise的状态处于pending时,then方法会建立PromiseReactions链表。
注意:
- promise链式调用是基于新创建的JSPromise实例
- 如果JSPromise实例已经从pending状态变成fulfilled/rejected状态,意味着在调用then方法之前resolve/reject方法已经触发,这时在then方法里会直接创建PromiseReactionJobTask并进入microtask队列等待,对应PerformPromiseThenImpl里状态判断if-else的else分支,这里先按下不表,因为then和resolve/reject方法对之间的关系还没揭开,留到后面详细说明。不过可以先简单了解下cxx源码中会根据不同状态创建PromiseReactionJobTask,fulfilled时为NewPromiseFulfillReactionJobTask;rejected时为NewPromiseRejectReactionJobTask
- 由2得到,resolve/reject方法对都是可以单独同步调用,这从下文也能得到佐证,这也是deferred的实现基础
这里暂且先对PromiseReactionJobTask保持印象,这是microtask队列中一种关于Promise的JobTask,后面还会提到另一种。
OK,在promise状态为pending且经过then方法调用后,PromiseReactions已经创建并建立,这时resolve方法触发,表现是状态从pending进入fulfilled。
cxx层面的ResolvePromise方法是resolve方法的built-in实现。
ResolvePromise接收两个入参:
- resolve方法的上下文context所对应的JSPromise实例
- 透传resolve方法的入参resolution
在ResolvePromise里,主要是针对resolve方法入参类型进行分支判断:
- 非对象,直接FulfillPromise
- 对象但非thenable,直接FulfillPromise
- thenable或为当前上下文的JSPromise,进入Enqueue,创建PromiseResolveThenableJobTask进microtask队列等待,这里先不重点分析这种情况,后续详细说明
这里出现的PromiseResolveThenableJobTask,和上面提到的PromiseReactionJobTask一样,
都是在microtask中关于Promise的JobTask。
注意:
目前onFulfilled handler都还没有执行,什么时候执行?什么方式执行? 关键就在FulfillPromise,JSPromise实例进入fulfilled状态的唯一触发点。
同时抛一个题,代号叫“看似简单的promise”,想一想下面这段代码输出会是什么:
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方法调用先绑定先执行,那不就输出:
0
4
1
2
3
答案对吗?你对handler的执行时机和方式真的了解了吗?
队列等待
现在假设resolve入参为非thenable类型,即直接进入FulfillPromise的调用逻辑。
在FulfillPromise中,会将遍历PromiseReactions链表生成的一个个PromiseReactionJobTask放入microtask队列等待执行,这种PromiseReactionJobTask创建场景是最常见的,也就是说then方法调用在resolve方法之前。
- 判断当前promise状态值是否处于pending,这点尤为重要,一旦promise状态发生变化意味着resolve/reject方法失去意义,即这个时候调用resolve/reject方法将不会有任何副作用
- 缓存当前JSPromise的reactions_or_result,这时的reactions_or_result为PromiseReactions链表,存放着promise chain信息
- 将reactions_or_result值设置为resolve入参,对应js层面就是设置promise.[[PromiseResult]];同时设置JSPromise状态值为fulfilled,对应js层面就是设置promise.[[PromiseState]]。这里也看出,先设置状态再触发PromiseReactions。
- TriggerPromiseReactions,遍历PromiseReactions分别创建PromiseReactionJobTask。
注意:
从PerformPromiseThenImpl可以看出,对已经不处于pending状态的promise多次调用then方法等价于往microtask队列里放入task,且handler接收到的入参值都是promise.reactions_or_result,因为这个值经由FulfillPromise逻辑处理已经锁定
再看TriggerPromiseReactions,将当前resolve入参透传到MorphAndEnqueuePromiseReaction:
reactionType根据resolve/reject方法确定,传递对应的fulfilled/rejected。
- 因为上文提到PromiseReactions链表是反序的,所以要先反转重新生成顺序正确的PromiseReactions
- 遍历顺序正确的PromiseReactions,分别MorphAndEnqueuePromiseReaction
MorphAndEnqueuePromiseReaction逻辑简单清晰,针对当前promise状态分别创建对应的PromiseReactionJobTask并放入microtask队列等待。
注意:
此时PromiseReactionJobTask所存的argument属性值就是由resolve方法入参经FulfillPromise再经TriggerPromiseReactions一路透传,一直到MorphAndEnqueuePromiseReaction。
如下图,PromiseReactions经过反转得到了最后handler正确顺序执行的结果。
经过FulfillPromise,onFulfilled handler执行方式以及执行时机就非常清晰了:
- 执行方式:所有的onFulfilled hanlder都是在microtask队列中异步执行,
- 执行时机:只有在resolve方法调用后才开始被放入microtask队列等到
回过头再看“看似简单的promise”这道题,现在应该有不同答案了:
0
1
2
4
3
但是这答案对吗?🤔这里可是return Promise.resolve(4)呢?
注意:
对于promise的reject方法逻辑链路,它和resolve方法逻辑链路基本一致,由RejectPromise触发,将promise状态设置为rejected,而此时TriggerPromiseReactions传递的reactionType是rejected。
特别注意的是RejectPromise也同样会创建PromiseReactionJob,但是PromiseReactionJob中onRejected handler执行完照样会进入promise chain下一次的FuflfillPromiseReactionJob,这就是为什么reject触发之后,下一跳的onFulfilled handler会触发的原因所在。
异步执行
PromiseReactionJob
随着事件循环Event-Loop,microtask队列开始先进先出的执行所有的JobTask。上面提到对于Promise,有两种JobTask:
- PromiseReactionJobTask,创建PromiseReactionJob
- PromiseResolveThenableJobTask,创建PromiseResolveThenableJob
注意:
上文提到在promise不处于pending状态时,调用then方法会根据promise当前状态分别创建JobTask放入microtask队列:
-
fulfilled对应NewPromiseFulfillReactionJobTask,会创建PromiseFulfillReactionJob
-
rejected对应NewPromiseRejectReactionJobTask,会创建PromiseRejectReactionJob
底层调用的也还是PromiseReactionJob:
归根结底,最后所有的handler处理逻辑都是在PromiseReactionJob中被触发:
针对handler是否存在分成两个逻辑分支:
- handler存在,这是常规场景,将一路透传保存下来的resolve入参,传入handler执行
- handler执行成功,需对promise chain的下一跳PromiseCapacity进行“resolve”,而此时的入参就是handler的返回值,也就是调用PromiseCapacity.resolve,再次进入新一轮的ResolvePromise,只不过这次对应的JSPromise变成了PromiseCapacity.promise,就这样,promise chain逐次传递。
- 执行失败,则直接调用PromiseCapacity.reject,进入rejected状态
- handler不存在,先按下不表,这种情况只有在PromiseResolveThenableJob里存在,下面说
PromiseResolveThenableJob
上文假设resolve入参为非thenable类型,即直接进入FulfillPromise的调用逻辑。
但是假设handler的返回值是一个thenable对象会怎么样,意味着PromiseCapacity.resolve(thenable),意味着这时ResolvePromise接收到的resolution参数是一个thenable,此时内部逻辑分支会进入Enqueue:
着重看一下NewPromiseResolveThenableJobTask的参数:
-
promise,当前resolve方法归属的promise,针对PromiseCapacity,这里promise就是PromiseCapacity.promise,命名为promiseToResolve,代表将要处理的promise实例
-
resolution,handler返回的thenable对象,带then方法的普通对象或者是一个JSPromise实例,称呼它为promise中间产物
-
then,这个thenable的then方法,callable的普通函数或者Promise.prototype.then
最后得到PromiseResolveThenableJobTask实例并同样被放入microtask队列等待:
那这个临时生成的promise中间产物如何和当前的PromiseCapacity.promise产生关联,为什么thenable resolve之后PromiseCapacity.promise也会同样resolve?接着看,下面解答。
现在在microtask队里,轮到PromiseResolveThenableJob执行了:
如果handler返回的thenable是一个JSPromise实例,则其then方法就是Promise.prototype.then,和context的then方法是相同
- 则针对thenable进行一次then方法调用,即PerformPromiseThenImpl,而promiseToResolve被作为这一次调用的临时PromiseCapability
- 而且此时的onFulfilled/onRejected handlers设置为空,handler为空是这次then方法调用的最鲜明特点,后续有重用
- 而等到这个中间产物resolve时,又是一次由ResolvePromise逻辑触发的PromiseReactionJob,此时由于handler不存在,直接进FuflfillPromiseReactionJob
- FuflfillPromiseReactionJob里,promise chain中的下一跳promiseToResolve就会进入ResolvePromise,然后又进入PromiseReactionJob,promise chain开始逐次传递
如果handler返回的thenable只是一个带有then方法的普通对象
- 则通过promiseToResolve创建resolve/reject方法对
- 并以这个thenable为上下文context调用普通对象的then方法,传入resolve/reject方法对,最终达到promiseToResolve.resolve的效果
如下面这个例子
Promise.resolve().then(() => {
return {
then(resolve, reject) {
resolve(1)
}
}
}).then((res) => console.log(res));
// 1
注意:
现在弄明白当handler返回一个thenable对象时,会先在microtask队列里放入一个PromiseResolveThenableJobTask,然后经由PromiseResolveThenableJob又在microtask队列里放入PromiseReactionJobTask。那么现在回头看“看似简单的promise”这道题,正确答案应该是
0
1
2
3
4
因为return Promise.resolve(4)导致在microtask队列里多放入了一个PromiseResolveThenableJobTask,使得console.log(3)这个handler所在的PromiseReactionJobTask先于console.log(res),而microtask队列先进先出,所以先输出了3,后输出4
其他方法
Promise.resolve
Promise.resolve()返回的是一个创建完成就进入fulfilled状态的JSPromise实例。
如果入参不是thenable,直接进入NeedToAllocate代码逻辑块处理,
- 如果当前上下文中的Promise构造器为cxx原生Promise构造器,NewJSPromise生成promise实例并对其ResolvePromise,此时该该promise已经处于fulfilled状态,且值锁定为value
- 通过当前上下文中的Promise构造器创建PromiseCapability并直接调用PromiseCapability.resolve(value),最后返回PromiseCapability.promise,此时该PromiseCapability.promise已经处于fulfilled状态,且值锁定为value
如果入参为thenable
- 当该thenable为JSPromise实例,即它的构造器和cxx原生Promise构造器相同,直接返回这个JSPromise
- 如果该thenable不是JSPromise实例,且不为cxx原生Promise的子类,进入NeedToAllocate代码逻辑块处理
后续所有处理就是针对这个返回的JSPromise实例,但是由于它的状态已经变成fulfilled,所以then方法调用时会直接创建PromiseReactionJobTask。PromiseReactionJobTask的执行上文已经分解过了。
比如下面这段代码:
Promise.resolve(1).then((res) => { console.log(res); })
.then((res) => { console.log(res); })这个then调用时直接将(res) => { console.log(res); }这个onFulfilled handler包装成PromiseReactionJobTask
Promise.reject
Promise.reject()返回一个创建完成并进入rejected状态的JSPromise实例。
- 对reject静态方法调用的上下文context如果不是cxx原生Promise构造器,NewPromiseCapability创建PromiseCapability并直接调用PromiseCapability.reject(reason),返回PromiseCapability.promise
- 如果是Promise.reject的方式去调用,则由NewJSPromise(PromiseState::kRejected, reason)生成一个状态为rejected的JSPromise,并交由runtime::PromiseRejectEventFromStack(promise, reason)去处理,因为一个状态为rejected的JSPromise如果没有添加任何onRejected handler时,需要由runtime去帮助捕获错误
Promise.all
Promise.all入参可以接收一个iterable,该iterable迭代器的每一项可以是JSPromise/thenable/普通值。
当通过Promise.all方式调用,当前all方法里的上下文receiver就是cxx原生Promise构造器,通过它进行NewPromiseCapability生成PromiseCapability,此时promiseResolveFunction即为built-in的resolve方法。
进入PerformPromiseAll方法后,对接收到的迭代器iterator遍历处理,每一次迭代器遍历时:
-
获取迭代器next value
-
根据当前迭代器所处index,创建PromiseResolvingFunctions,并绑定对应的index到resolve/reject方法上。此处的resolve/reject方法对是游离存在的,但是都和同一个resolveElementContext关联
-
根据next value值的类型进行逻辑分支,这里稍稍和ResolvePromise里碰到的值类型判断有些许区别,大致可以理解为一次ResolvePromise(promise, nextValue)
-
如果next value不为thenable,直接通过Promise.resolve(nextValue)方式创建一个fulfilled状态的JSPromise实例,此时针对该JSPromise调用其then方法,传入上面创建的一对游离resolve/reject方法分别作为onFulfilled/onRejected handlers。则由于JSPromise实例状态已为fulfilled,则作为onFulfilled handler的resolve方法会被触发
-
当next value是一个thenable时,将对其进行PerformPromiseThenImpl,也就是调用thenable.then,而且此时创建的这对游离resolve/reject方法也会被作为onFulfilled/onRejected handlers传入,意味着resolve/reject方法会在PromiseReactionJob中被触发
-
当这些临时根据index创建的游离resolve/reject方法对被触发后,会将对应的结果值存入*NativeContextSlot(nativeContext, ContextSlot::JS_ARRAY_PACKED_ELEMENTS_MAP_INDEX)所对应的arrayMap,在js层面,简单理解就是存放入一个堆内存上。最后通过PromiseCapability.resolve来触发
注意:
在任意一次迭代器结果获取中出错,对应的PromiseCapability.promise都会进入rejected,所以针对于Promise.all,只有迭代器中所有结果值都resolve才能进入fulfilled状态。
结尾
经过以上分析,现在彻底明白Promise实现原理:
- 基于microtask队列实现异步执行
- 通过PromiseCapability的deferred模式,完美实现promise chain
在此基础上,Promise实现逻辑中关于把handler转化为PromiseReactionJobTask并放入microtask队列的巧思,既是对Event-Loop机制的保护,也是对js引擎单线程特点的充分利用。
那么,关于js异步调用的认识,你有哪些想法呢,欢迎留言讨论。
参考资料
- ECMA Promise:tc39.es/ecma262/#se…
- V8源码:github.com/v8/v8
- Chromium Code Search:source.chromium.org/chromium/ch…
转载自:https://juejin.cn/post/7201047960834588730