Promise V8 源码分析

Posted 悬笔e绝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Promise V8 源码分析相关的知识,希望对你有一定的参考价值。

基于 node 版本 14.13.0,V8 版本 8.4.371。本文介绍的内容是 reject、catch 和 then 的链式调用。

reject

 
   
   
 
  1. new Promise((resolve, reject) => {

  2. setTimeout(_ => reject('rejected'), 5000)

  3. }).then(_ => {

  4. console.log('fulfilled')

  5. }, reason => {

  6. console.log(reason)

  7. })

上述代码 5s 后执行 reject 函数,控制台打印 rejected。reject 函数调用了 V8 的 RejectPromise 函数,源码如下:

 
   
   
 
  1. transitioning builtin

  2. RejectPromise(implicit context: Context)(

  3. promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {

  4. // 取出 Promise 的处理对象 PromiseReaction

  5. const reactions =

  6. UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);

  7. // 这里的 reason 就是 reject 函数的参数

  8. promise.reactions_or_result = reason;

  9. // 设置 Promise 的状态为 rejected

  10. promise.SetStatus(PromiseState::kRejected);

  11. TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);

  12. return Undefined;

  13. }

TriggerPromiseReactions 函数在上一篇文章分析过,功能是将 Promise 处理函数相关的 PromiseReaction 链表,反转后依次插入 V8 的 microtask 队列,TriggerPromiseReactions 源码继续删减如下:

 
   
   
 
  1. // https://tc39.es/ecma262/#sec-triggerpromisereactions

  2. transitioning macro TriggerPromiseReactions(implicit context: Context)(

  3. reactions: Zero|PromiseReaction, argument: JSAny,

  4. reactionType: constexpr PromiseReactionType): void {

  5. // 删减了链表反转的代码

  6. let current = reactions;

  7. // reactions 是一个链表,下面的 while 循环遍历链表

  8. while (true) {

  9. typeswitch (current) {

  10. case (Zero): {

  11. break;

  12. }

  13. case (currentReaction: PromiseReaction): {

  14. // 取出链表下一个结点

  15. current = currentReaction.next;

  16. // 调用 MorphAndEnqueuePromiseReaction,将当前节点插入 microtask 队列

  17. MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType);

  18. }

  19. }

  20. }

  21. }

MorphAndEnqueuePromiseReaction 将 PromiseReaction 转为 microtask,最终插入 microtask 队列,morph 本身有转变/转化的意思,比如 Polymorphism (多态)。

MorphAndEnqueuePromiseReaction 接收 3 个参数,PromiseReaction 是前面提到的包装了 Promise 处理函数的链表对象,argument 是 resolve/reject 的参数,reactionType 表示 Promise 最终的状态,fulfilled 状态对应的值是 kPromiseReactionFulfill,rejected 状态对应的值是 kPromiseReactionReject。MorphAndEnqueuePromiseReaction 的逻辑很简单,因为此时已经知道了 Promise 的最终状态,所以可以从 promiseReaction 对象得到 promiseReactionJobTask 对象,promiseReactionJobTask 的变量命名与 ECMA 规范相关描述一脉相承,其实就是传说中的 microtask。MorphAndEnqueuePromiseReaction 源码如下,仅保留了和本小节相关的内容。

 
   
   
 
  1. transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(

  2. promiseReaction: PromiseReaction, argument: JSAny,

  3. reactionType: constexpr PromiseReactionType): void {

  4. let primaryHandler: Callable|Undefined;

  5. let secondaryHandler: Callable|Undefined;

  6. if constexpr (reactionType == kPromiseReactionFulfill) {

  7. primaryHandler = promiseReaction.fulfill_handler;

  8. secondaryHandler = promiseReaction.reject_handler;

  9. } else {

  10. primaryHandler = promiseReaction.reject_handler;

  11. secondaryHandler = promiseReaction.fulfill_handler;

  12. }

  13. const handlerContext: Context =

  14. ExtractHandlerContext(primaryHandler, secondaryHandler);

  15. if constexpr (reactionType == kPromiseReactionFulfill) {

  16. // 删

  17. } else {

  18. * UnsafeConstCast(& promiseReaction.map) =

  19. PromiseRejectReactionJobTaskMapConstant();

  20. const promiseReactionJobTask =

  21. UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);

  22. // argument 是 reject 的参数

  23. promiseReactionJobTask.argument = argument;

  24. promiseReactionJobTask.context = handlerContext;

  25. // handler 是 JS 层面 then 方法的第二个参数,或 catch 方法的参数

  26. promiseReactionJobTask.handler = primaryHandler;

  27. // promiseReactionJobTask 就是那个工作中经常被反复提起的 microtask

  28. // EnqueueMicrotask 将 microtask 插入 microtask 队列

  29. EnqueueMicrotask(handlerContext, promiseReactionJobTask);

  30. }

  31. }

reject 和 resolve 的逻辑基本相同,分为 3 步:

  • 设置 Promise 的 value/reason,也就是 resolve/reject 的参数

  • 设置 Promise 的状态:fulfilled/rejected

  • 从之前调用 then/catch 方法时收集到的依赖,也就是 promiseReaction 对象,得到一个个 microtask,最后将 microtask 插入 microtask 队列

catch

 
   
   
 
  1. new Promise((resolve, reject) => {

  2. setTimeout(reject, 2000)

  3. }).catch(_ => {

  4. console.log('rejected')

  5. })

以上面代码为例,当 catch 方法执行时,调用了 V8 的 PromisePrototypeCatch 方法,源码如下:

 
   
   
 
  1. transitioning javascript builtin

  2. PromisePrototypeCatch(

  3. js-implicit context: Context, receiver: JSAny)(onRejected: JSAny): JSAny {

  4. const nativeContext = LoadNativeContext(context);

  5. return UnsafeCast<JSAny>(

  6. InvokeThen(nativeContext, receiver, Undefined, onRejected));

  7. }

PromisePrototypeCatch 的源码确实只有就这几行,除了调用 InvokeThen 方法再无其它 。从名字可以推测出,InvokeThen 调用的是 Promise 的 then 方法,InvokeThen 源码如下:

 
   
   
 
  1. transitioning

  2. macro InvokeThen<F: type>(implicit context: Context)(

  3. nativeContext: NativeContext, receiver: JSAny, arg1: JSAny, arg2: JSAny,

  4. callFunctor: F): JSAny {

  5. if (!Is<Smi>(receiver) &&

  6. IsPromiseThenLookupChainIntact(

  7. nativeContext, UnsafeCast<HeapObject>(receiver).map)) {

  8. const then =

  9. UnsafeCast<JSAny>(nativeContext[NativeContextSlot::PROMISE_THEN_INDEX]);

  10. // 重点在下面一行,调用 then 方法并返回,两个分支都一样

  11. return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);

  12. } else

  13. deferred {

  14. const then = UnsafeCast<JSAny>(GetProperty(receiver, kThenString));

  15. // 重点在下面一行,调用 then 方法并返回,两个分支都一样

  16. return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);

  17. }

  18. }

InvokeThen 方法有 if/else 两个分支,两个分支的逻辑差不多,本小节的 JS 示例代码走的是 if 分支。先是拿到 V8 原生的 then 方法,然后通过 callFunctor.Call(nativeContext, then, receiver, arg1, arg2) 调用 then 方法。then 方法上一篇文章有提及,这里不再赘述。

既然 catch 方法底层调用了 then 方法,那么 catch 方法也有和 then 方法一样的返回值,catch 方法可以继续抛出异常,可以继续链式调用。

 
   
   
 
  1. new Promise((resolve, reject) => {

  2. setTimeout(reject, 2000)

  3. }).catch(_ => {

  4. throw 'rejected'

  5. }).catch(_ => {

  6. console.log('last catch')

  7. })

上面的代码第 2 个 catch 捕获第 1 个 catch 抛出的异常,最后打印 last catch。

catch 方法通过底层调用 then 方法来实现 假如 obj 是一个 Promise 对象,JS 层面 obj.catch(onRejected) 等价于 obj.then(undefined, onRejected)

then 的链式调用与 microtask 队列

 
   
   
 
  1. Promise.resolve('123')

  2. .then(() => {throw new Error('456')})

  3. .then(_ => {

  4. console.log('shouldnot be here')

  5. })

  6. .catch((e) => console.log(e))

  7. .then((data) => console.log(data));

以上代码运行后,打印 Error: 456 和 undefined。为了便于叙述,将 then 的链式调用写法改为啰嗦写法。

 
   
   
 
  1. const p0 = Promise.resolve('123')

  2. const p1 = p0.then(() => {throw new Error('456')})

  3. const p2 = p1.then(_ => {

  4. console.log('shouldnot be here')

  5. })

  6. const p3 = p2.catch((e) => console.log(e))

  7. const p4 = p3.then((data) => console.log(data));

then 方法返回新的 Promise,所以 p0、p1、p2、p3 和 p4 这 5 个 Promise 互不相等。

p0 开始便处于 fulfilled 状态,当执行

 
   
   
 
  1. const p1 = p0.then(() => {throw new Error('456')})

时,由于 p0 已是 fulfilled 状态,直接将 p0 的 fulfilled 处理函数插入 microtask 队列,此时 microtask 队列简略示意图如下,绿色区域表示 microtask,蓝色区域表示 microtask 队列。

跑完余下所有的代码。

 
   
   
 
  1. const p1 = p0.then(() => {throw new Error('456')})

  2. const p2 = p1.then(_ => {

  3. console.log('shouldnot be here')

  4. })

  5. const p3 = p2.catch((e) => console.log(e))

  6. const p4 = p3.then((data) => console.log(data));

p1、p2、p3 和 p4 这 4 个 Promise 都处于 pending 状态,microtask 队列还是

Promise V8 源码分析(二)

开始执行 microtask 队列,核心方法是 MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask,代码是用 CodeStubAssembler 写的,代码很长,逻辑简单,评论区经常有提看不懂 CodeStubAssembler 这种类汇编语言,这里就不再贴代码了,预计之后的版本 V8 会用 Torque 重写。

在执行 microtask 的过程中,MicrotaskQueueBuiltinsAssembler::RunSingleMicrotask 会调用 PromiseReactionJob,源码如下:

 
   
   
 
  1. transitioning

  2. macro PromiseReactionJob(

  3. context: Context, argument: JSAny, handler: Callable|Undefined,

  4. promiseOrCapability: JSPromise|PromiseCapability|Undefined,

  5. reactionType: constexpr PromiseReactionType): JSAny {

  6. if (handler == Undefined) {

  7. // 没有处理函数的 case,透传上一个 Promise 的 argument 和状态

  8. if constexpr (reactionType == kPromiseReactionFulfill) {

  9. // 基本类同 JS 层的 resolve

  10. return FuflfillPromiseReactionJob(

  11. context, promiseOrCapability, argument, reactionType);

  12. } else {

  13. // 基本类同 JS 层的 reject

  14. return RejectPromiseReactionJob(

  15. context, promiseOrCapability, argument, reactionType);

  16. }

  17. } else {

  18. try {

  19. // 试图调用 Promise 处理函数,相当于 handler(argument)

  20. const result =

  21. Call(context, UnsafeCast<Callable>(handler), Undefined, argument);

  22. // 基本类同 JS 层的 resolve

  23. return FuflfillPromiseReactionJob(

  24. context, promiseOrCapability, result, reactionType);

  25. } catch (e) {

  26. // 基本类同 JS 层的 reject

  27. return RejectPromiseReactionJob(

  28. context, promiseOrCapability, e, reactionType);

  29. }

  30. }

  31. }

PromiseReactionJob 接收的参数和 microtask 密切相关,当下 argument 参数是 '123',handler 是函数 () => {throw new Error('456')},promiseOrCapability 是 p1,reactionType 是 kPromiseReactionFulfill。

handler 有值,进入 else 分支,在 try...catch 包裹下,试图调用 handler。handler 里 throw new Error('456') 抛出异常,被 catch 捕捉,调用 RejectPromiseReactionJob 方法,从函数名字也可以看出,p1 最终状态为 rejected。后面的代码和 JS 层面直接调用 reject 代码差不多,向 microtask 队列插入一个 microtask,这里不再赘述。当前 microtask 执行完毕后,会从 microtask 队列移除。

新增一个新 microtask,移除一个旧 microtask 后,microtask 队列简略示意图如下:

Promise V8 源码分析(二)

handler 为 undefined 的原因是 p1 的最终状态是 rejected,但却没有 rejected 状态的处理函数。

开始执行下一个 microtask,还是调用上文提到的 PromiseReactionJob,argument 参数为 Error('456'),handler 是 undefined,promiseOrCapability 是 p2,reactionType 是 kPromiseReactionReject。由于 handler 是 undefined,这一次走的是 if 分支,最终调用了 RejectPromiseReactionJob,将 p2 状态置为 rejected。p1 相当于一个中转站,收到了 Error('456'),自己没有相应状态的处理函数,把从 p0 收到的 Error('456') 和 rejected 状态继续向下传给了 p2。执行完当前 microtask 后,microtask 队列的简略示意图如下:

Promise V8 源码分析(二)

还是执行下一个 microtask,还是调用 PromiseReactionJob,argument 是 Error('456'),handler 是 (e) => console.log(e),promiseOrCapability 是 p3,reactionType 是 kPromiseReactionReject。在 try...catch 中试图 handler,handler 不再抛异常,打印 Error('456'),返回 undefined。最后调用 FuflfillPromiseReactionJob,使 p3 最终状态是 fulfilled。执行完当前 microtask 后,microtask 队列的简略示意图如下:

Promise V8 源码分析(二)

后面的流程和之前一样,就不解释了,上一个 microtask 的 handler (e) => console.log(e) 的返回值是 undefined,所以 (data) => console.log(data) 打印 undefined。

执行完所有 microtask 后,p0、p1、p2、p3 和 p4 状态如下,图是从浏览器控制台截的。

回头再看这段代码,catch 在这里的作用相当于是把一个 rejected 状态的 Promise 链路,恢复成 fulfilled 状态,使后面的处理函数 (data)=> console.log(data) 得到执行的机会。

 
   
   
 
  1. // 链式调用,每一级接收上一级的 argument 和状态(fulfilled/rejected)

  2. // 调用本级的 handler,将本级的 argument 和状态传给下一级

  3. // 有点类似数组的 reduce 方法

  4. Promise.resolve('123')

  5. .then(() => {throw new Error('456')})

  6. .then(_ => {

  7. console.log('shouldnot be here')

  8. })

  9. // catch 在这里的作用相当于是把一个 rejected 状态的 Promise 链路

  10. // 恢复成 fulfilled 状态

  11. .catch((e) => console.log(e))

  12. .then((data) => console.log(data));

总结与感想

本文看似篇幅略长,其实大部分内容是 Promise A+ 规范的 2.2.7 节,规范简直字字珠玑,膜拜。


以上是关于Promise V8 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

从V8源码分析一个JS 数组的内存占用问题

VSCode自定义代码片段12——JavaScript的Promise对象

VSCode自定义代码片段12——JavaScript的Promise对象

鸿蒙源码分析系列(总目录) | 百万汉字注解 百篇博客分析 | 百篇博客分析鸿蒙源码 | v8.21

如何在V8中优化JavaScript异步编程?

Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段