前言
2022年,文章写少了,源码也看少了,进入2023年,就希望能坚持写作,把每一次源码阅读的过程都记录下来,最后收到《这就是编程》系列中。
“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”
今天带来关于V8 Promise在cxx语言层面的源码分析,一文带你全面了解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对executor执行过程进行错误捕获
- 最后返回JSPromise实例
注意:
在Promise构建过程中executor是同步执行,同时由于cxx内部会有try-catch对执行过程错误进行捕获,所以executor里一旦抛错同样能被onRejected handler处理,js层面不必对executor添加额外的try-catch
这里抛一个题,代号叫“看似简单的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方法调用先绑定的handler先执行,那不就输出:
0
4
1
2
3
你认为答案对吗?你对handler的执行时机和方式真的了解了吗?
绑定回调
得到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链表里会保存每一次调用传入的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链表其实顺序刚好相反,笔者疑惑cxx层面为什么不在建立PromiseReactions链表时就按正确顺序呢?先埋个伏笔,看看后续cxx对这个顺序如何处理。
OK,接着往下,其实then方法核心逻辑在PerformPromiseThenImpl里。
当promise的状态处于pending时,then方法才会建立PromiseReactions链表。
注意:
- promise链式调用是基于新创建的JSPromise实例
- 如果JSPromise实例已经从pending状态变成fulfilled/rejected状态,意味着在调用then方法之前resolve/reject方法已经触发,这时在then方法里会直接创建PromiseReactionJobTask并进入microtask队列等待,对应PerformPromiseThenImpl里状态判断if-else的else分支,这里先不展开,后文分解,不过可以先简单了解下cxx源码中会根据不同状态创建不同的PromiseReactionJobTask,状态为fulfilled时NewPromiseFulfillReactionJobTask,状态为rejected时NewPromiseRejectReactionJobTask
- 由2得到,resolve/reject方法对都是可以单独同步调用,这从下文也能得到佐证,这也是deferred的实现基础
这里暂且先对PromiseReactionJobTask保持印象,这是microtask队列中一种关于Promise的JobTask,后面还会提到另一种。某种意义上说JobTask也是Promise核心机制之一。
OK,接着往下,在promise状态为pending且经过then方法调用后,PromiseReactions已经创建并建立,这时resolve方法触发,表现是状态从pending进入fulfilled。
cxx层面的ResolvePromise方法是resolve方法的built-in实现,前面提到在Promise构造方法调用时会创建PromiseResolvingFunctions,得到的resolve方法就是ResolvePromise这个built-in方法。
ResolvePromise接收两个入参:
- resolve方法的上下文context所对应的JSPromise实例
- 作为promise.reactions_or_result值的resolution入参
在ResolvePromise里,主要是针对resolve方法入参类型进行分支判断:
- 非对象,直接FulfillPromise
- 对象但非thenable,直接FulfillPromise
- thenable,进入Enqueue,创建PromiseResolveThenableJobTask放入microtask队列等待,这里先不重点分析这种情况,下文详细说明
这里出现的PromiseResolveThenableJobTask,和上面提到的PromiseReactionJobTask一样,
都是在microtask队列中关于Promise的JobTask。
注意:
目前onFulfilled handler都还没有执行,那么什么时候执行,什么方式执行,关键就在FulfillPromise里,它也是JSPromise实例进入fulfilled状态的唯一触发点。
队列等待
现在假设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,同时根据resolve/reject方法的调用设置reactionType值,分别传递fulfilled/rejected。
- 因为上文提到PromiseReactions链表是反序的,所以要先反转重新生成顺序正确的PromiseReactions
- 遍历顺序正确的PromiseReactions,分别对每一个PromiseReaction进行MorphAndEnqueuePromiseReaction逻辑处理,就是根据当前promise状态,分别对其创建对应的PromiseReactionJobTask并放入microtask队列等待,由于microtask队列是先进先出,所以前面才要反转PromiseReactions链表,这样先绑定的handler才能先出栈执行。笔者认为cxx层面在建立PromiseReactions链表时完全可以一步到位直接建立一个顺序正确的链表,不知道cxx层面为什么没这么做?