前言

回顾下。今日前端早读课文章由微店@徐鹏跃投稿分享。

正文从这开始~~

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

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

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

  3. }).then(_ => {

  4. console.log('fulfilled')

  5. }, reason => {

  6. console.log(reason)

  7. })

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

  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. }

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

  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. }

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

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

  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. }

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

catch

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

  2. setTimeout(reject, 2000)

  3. }).catch(_ => {

  4. console.log('rejected')

  5. })

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

  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. }

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

  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. }

方法有 if/else 两个分支,两个分支的逻辑差不多,本小节的 JS 示例代码走的是 if 分支。先是拿到 V8 原生的 then 方法,然后通过 .Call(, then, , 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 是一个 对象,JS 层面 obj.catch() 等价于 obj.then(, )

then 的链式调用与 队列

  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 和 。为了便于叙述,将 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 方法返回新的 ,所以 p0、p1、p2、p3 和 p4 这 5 个 互不相等。

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

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

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

源码编程器4.0手机版_promise源码_源码网

跑完余下所有的代码。

  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 个 都处于 状态, 队列还是

源码编程器4.0手机版_promise源码_源码网

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

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

  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. }

接收的参数和 密切相关,当下 参数是 '123', 是函数 () => {throw new Error('456')}, 是 p1, 是 ill。

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

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

promise源码_源码编程器4.0手机版_源码网

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

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

源码编程器4.0手机版_源码网_promise源码

还是执行下一个 ,还是调用 , 是 Error('456'),是 (e) => .log(e)promise源码, 是 p3, 是 ct。在 try...catch 中试图 , 不再抛异常,打印 Error('456'),返回 。最后调用 ,使 p3 最终状态是 。执行完当前 后, 队列的简略示意图如下:

源码编程器4.0手机版_源码网_promise源码

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

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

源码编程器4.0手机版_promise源码_源码网

回头再看这段代码,catch 在这里的作用相当于是把一个 状态的 链路,恢复成 状态,使后面的处理函数 (data)=> .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));

总结与感想

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

promise源码_源码编程器4.0手机版_源码网

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注