前言
回顾下。今日前端早读课文章由微店@徐鹏跃投稿分享。
正文从这开始~~
基于 node 版本 14.13.0,V8 版本 8.4.371。本文介绍的内容是 、catch 和 then 的链式调用。
new Promise((resolve, reject) => {
setTimeout(_ => reject('rejected'), 5000)
}).then(_ => {
console.log('fulfilled')
}, reason => {
console.log(reason)
})
上述代码 5s 后执行 函数,控制台打印 。 函数调用了 V8 的 函数,源码如下:
transitioning builtin
RejectPromise(implicit context: Context)(
promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
// 取出 Promise 的处理对象 PromiseReaction
const reactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// 这里的 reason 就是 reject 函数的参数
promise.reactions_or_result = reason;
// 设置 Promise 的状态为 rejected
promise.SetStatus(PromiseState::kRejected);
TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);
return Undefined;
}
ons 函数在上一篇文章分析过,功能是将 处理函数相关的 链表,反转后依次插入 V8 的 队列,ons 源码继续删减如下:
// https://tc39.es/ecma262/#sec-triggerpromisereactions
transitioning macro TriggerPromiseReactions(implicit context: Context)(
reactions: Zero|PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
// 删减了链表反转的代码
let current = reactions;
// reactions 是一个链表,下面的 while 循环遍历链表
while (true) {
typeswitch (current) {
case (Zero): {
break;
}
case (currentReaction: PromiseReaction): {
// 取出链表下一个结点
current = currentReaction.next;
// 调用 MorphAndEnqueuePromiseReaction,将当前节点插入 microtask 队列
MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType);
}
}
}
}
将 转为 ,最终插入 队列,morph 本身有转变/转化的意思,比如 (多态)。
接收 3 个参数, 是前面提到的包装了 处理函数的链表对象, 是 / 的参数, 表示 最终的状态, 状态对应的值是 ill, 状态对应的值是 ct。 的逻辑很简单,因为此时已经知道了 的最终状态,所以可以从 对象得到 sk 对象,sk 的变量命名与 ECMA 规范相关描述一脉相承,其实就是传说中的 。 源码如下,仅保留了和本小节相关的内容。
transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(
promiseReaction: PromiseReaction, argument: JSAny,
reactionType: constexpr PromiseReactionType): void {
let primaryHandler: Callable|Undefined;
let secondaryHandler: Callable|Undefined;
if constexpr (reactionType == kPromiseReactionFulfill) {
primaryHandler = promiseReaction.fulfill_handler;
secondaryHandler = promiseReaction.reject_handler;
} else {
primaryHandler = promiseReaction.reject_handler;
secondaryHandler = promiseReaction.fulfill_handler;
}
const handlerContext: Context =
ExtractHandlerContext(primaryHandler, secondaryHandler);
if constexpr (reactionType == kPromiseReactionFulfill) {
// 删
} else {
* UnsafeConstCast(& promiseReaction.map) =
PromiseRejectReactionJobTaskMapConstant();
const promiseReactionJobTask =
UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);
// argument 是 reject 的参数
promiseReactionJobTask.argument = argument;
promiseReactionJobTask.context = handlerContext;
// handler 是 JS 层面 then 方法的第二个参数,或 catch 方法的参数
promiseReactionJobTask.handler = primaryHandler;
// promiseReactionJobTask 就是那个工作中经常被反复提起的 microtask
// EnqueueMicrotask 将 microtask 插入 microtask 队列
EnqueueMicrotask(handlerContext, promiseReactionJobTask);
}
}
和 的逻辑基本相同,分为 3 步:
catch
new Promise((resolve, reject) => {
setTimeout(reject, 2000)
}).catch(_ => {
console.log('rejected')
})
以上面代码为例,当 catch 方法执行时,调用了 V8 的 h 方法,源码如下:
transitioning javascript builtin
PromisePrototypeCatch(
js-implicit context: Context, receiver: JSAny)(onRejected: JSAny): JSAny {
const nativeContext = LoadNativeContext(context);
return UnsafeCast<JSAny>(
InvokeThen(nativeContext, receiver, Undefined, onRejected));
}
h 的源码确实只有就这几行,除了调用 方法再无其它 。从名字可以推测出, 调用的是 的 then 方法, 源码如下:
transitioning
macro InvokeThen<F: type>(implicit context: Context)(
nativeContext: NativeContext, receiver: JSAny, arg1: JSAny, arg2: JSAny,
callFunctor: F): JSAny {
if (!Is<Smi>(receiver) &&
IsPromiseThenLookupChainIntact(
nativeContext, UnsafeCast<HeapObject>(receiver).map)) {
const then =
UnsafeCast<JSAny>(nativeContext[NativeContextSlot::PROMISE_THEN_INDEX]);
// 重点在下面一行,调用 then 方法并返回,两个分支都一样
return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);
} else
deferred {
const then = UnsafeCast<JSAny>(GetProperty(receiver, kThenString));
// 重点在下面一行,调用 then 方法并返回,两个分支都一样
return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);
}
}
方法有 if/else 两个分支,两个分支的逻辑差不多,本小节的 JS 示例代码走的是 if 分支。先是拿到 V8 原生的 then 方法,然后通过 .Call(, then, , arg1, arg2) 调用 then 方法。then 方法上一篇文章有提及,这里不再赘述。
既然 catch 方法底层调用了 then 方法,那么 catch 方法也有和 then 方法一样的返回值,catch 方法可以继续抛出异常,可以继续链式调用。
new Promise((resolve, reject) => {
setTimeout(reject, 2000)
}).catch(_ => {
throw 'rejected'
}).catch(_ => {
console.log('last catch')
})
上面的代码第 2 个 catch 捕获第 1 个 catch 抛出的异常,最后打印 last catch。
catch 方法通过底层调用 then 方法来实现假如 obj 是一个 对象,JS 层面 obj.catch() 等价于 obj.then(, )
then 的链式调用与 队列
Promise.resolve('123')
.then(() => {throw new Error('456')})
.then(_ => {
console.log('shouldnot be here')
})
.catch((e) => console.log(e))
.then((data) => console.log(data));
以上代码运行后,打印 Error: 456 和 。为了便于叙述,将 then 的链式调用写法改为啰嗦写法。
const p0 = Promise.resolve('123')
const p1 = p0.then(() => {throw new Error('456')})
const p2 = p1.then(_ => {
console.log('shouldnot be here')
})
const p3 = p2.catch((e) => console.log(e))
const p4 = p3.then((data) => console.log(data));
then 方法返回新的 ,所以 p0、p1、p2、p3 和 p4 这 5 个 互不相等。
p0 开始便处于 状态,当执行
const p1 = p0.then(() => {throw new Error('456')})
时,由于 p0 已是 状态,直接将 p0 的 处理函数插入 队列,此时 队列简略示意图如下,绿色区域表示 ,蓝色区域表示 队列。
跑完余下所有的代码。
const p1 = p0.then(() => {throw new Error('456')})
const p2 = p1.then(_ => {
console.log('shouldnot be here')
})
const p3 = p2.catch((e) => console.log(e))
const p4 = p3.then((data) => console.log(data));
p1、p2、p3 和 p4 这 4 个 都处于 状态, 队列还是
开始执行 队列,核心方法是 ::,代码是用 写的,代码很长,逻辑简单,评论区经常有提看不懂 这种类汇编语言,这里就不再贴代码了,预计之后的版本 V8 会用 重写。
在执行 的过程中,:: 会调用 ,源码如下:
transitioning
macro PromiseReactionJob(
context: Context, argument: JSAny, handler: Callable|Undefined,
promiseOrCapability: JSPromise|PromiseCapability|Undefined,
reactionType: constexpr PromiseReactionType): JSAny {
if (handler == Undefined) {
// 没有处理函数的 case,透传上一个 Promise 的 argument 和状态
if constexpr (reactionType == kPromiseReactionFulfill) {
// 基本类同 JS 层的 resolve
return FuflfillPromiseReactionJob(
context, promiseOrCapability, argument, reactionType);
} else {
// 基本类同 JS 层的 reject
return RejectPromiseReactionJob(
context, promiseOrCapability, argument, reactionType);
}
} else {
try {
// 试图调用 Promise 处理函数,相当于 handler(argument)
const result =
Call(context, UnsafeCast<Callable>(handler), Undefined, argument);
// 基本类同 JS 层的 resolve
return FuflfillPromiseReactionJob(
context, promiseOrCapability, result, reactionType);
} catch (e) {
// 基本类同 JS 层的 reject
return RejectPromiseReactionJob(
context, promiseOrCapability, e, reactionType);
}
}
}
接收的参数和 密切相关,当下 参数是 '123', 是函数 () => {throw new Error('456')}, 是 p1, 是 ill。
有值,进入 else 分支,在 try...catch 包裹下,试图调用 。 里 throw new Error('456') 抛出异常,被 catch 捕捉,调用 nJob 方法,从函数名字也可以看出,p1 最终状态为 。后面的代码和 JS 层面直接调用 代码差不多,向 队列插入一个 ,这里不再赘述。当前 执行完毕后,会从 队列移除。
新增一个新 ,移除一个旧 后, 队列简略示意图如下:
为 的原因是 p1 的最终状态是 ,但却没有 状态的处理函数。
开始执行下一个 ,还是调用上文提到的 , 参数为 Error('456'), 是 , 是 p2, 是 ct。由于 是 ,这一次走的是 if 分支,最终调用了 nJob,将 p2 状态置为 。p1 相当于一个中转站,收到了 Error('456'),自己没有相应状态的处理函数,把从 p0 收到的 Error('456') 和 状态继续向下传给了 p2。执行完当前 后, 队列的简略示意图如下:
还是执行下一个 ,还是调用 , 是 Error('456'),是 (e) => .log(e)promise源码, 是 p3, 是 ct。在 try...catch 中试图 , 不再抛异常,打印 Error('456'),返回 。最后调用 ,使 p3 最终状态是 。执行完当前 后, 队列的简略示意图如下:
后面的流程和之前一样,就不解释了,上一个 的 (e) => .log(e) 的返回值是 ,所以 (data) => .log(data) 打印 。
执行完所有 后,p0、p1、p2、p3 和 p4 状态如下promise源码,图是从浏览器控制台截的。
回头再看这段代码,catch 在这里的作用相当于是把一个 状态的 链路,恢复成 状态,使后面的处理函数 (data)=> .log(data) 得到执行的机会。
// 链式调用,每一级接收上一级的 argument 和状态(fulfilled/rejected)
// 调用本级的 handler,将本级的 argument 和状态传给下一级
// 有点类似数组的 reduce 方法
Promise.resolve('123')
.then(() => {throw new Error('456')})
.then(_ => {
console.log('shouldnot be here')
})
// catch 在这里的作用相当于是把一个 rejected 状态的 Promise 链路
// 恢复成 fulfilled 状态
.catch((e) => console.log(e))
.then((data) => console.log(data));
总结与感想
本文看似篇幅略长,其实大部分内容是 A+ 规范的 2.2.7 节,规范简直字字珠玑,膜拜。