欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

V8 Promise源码全面解读

最编程 2024-02-09 17:39:49
...

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

写在前面的话

阅读本文你将收获什么?

  • 了解 V8 Promise 源码全过程,世界上不再有能困住你的 Promise 题目,我就是这么肯定这篇文章的干货
  • 仅仅了解或者实现了 Promise/A+ 规范,这与 JavaScript 的 Promise 中间还有很大的差距
  • 如果你在面试时将 Promise 回答到本文的深度,一定是收获 SP 或者 SSP offer 的利器,因为面试官大概率也不知道这些知识。

你知道 浏览器 & Node 中真正的 Promise 执行顺序是怎么样的吗,如果你只是看过 Promise/A+ 规范的 Promise 实现,那么我肯定的告诉你,你对 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(() => {
    console.log(5);
}).then(() => {
    console.log(6);
})
// 0 1 2 3 4 5 6


new Promise((resolve, reject) => {
    Promise.resolve().then(() => {
        resolve({
            then: (resolve, reject) => resolve(1)
        });
        Promise.resolve().then(() => console.log(2));
    });
}).then(v => console.log(v));
// 2 1

按照 Promise/A+ 规范来说,上面的代码打印的结果应该是 0 1 2 4 3 5 6,因为当 then 返回一个 Promise 的时候需要等待这个 Promise 完成后再同步状态和值给 then 的结果。

但是在 V8 甚至 各大支持 Promise 的主流浏览器中的执行结果都是 0 1 2 3 4 5 6

他们是如何做到与 Promise/A+ 规范不一样(也不能说一样,因为 Promise 没有明确描述他们的执行逻辑,只是给出一些规范)且保持一致的?

要知道, Promise 属于 JavaScript 中的一部分, 而 JavaScriptPromise 的实现规范并非来源于 Promise/A+,而是来自 ECMAScript 规范。

所以要知道这个问题的答案,我们不能仅仅看 Promise/A+ ,对于代码的执行过程和顺序我们的关注点应该是在 ECMAScript 或者 V8 上。

接下来我就结合 ECMAScript 规范来对 V8Promise 源码进行全面的解读。

还有三件事需要提前说一下:

  1. 本文更加适合有一定 Promise 基础的同学阅读,如果对 Promise 不了解的同学可以先看看这些文章
    • 【翻译】promise
    • Promise不会??看这里!!!史上最通俗易懂的Promise!!!
  2. 对于后续贴出的 c++ 代码大家只需要着重看带有中文注释的地方即可
  3. 因为代码块不会自动换行,所以建议 PC 端阅读可以有更好的阅读体验
  4. 文章很长,可以收藏,有时间静下心来慢慢看也可以。
  5. 点赞????????、点赞????????、点赞????????

进入正题

PromiseState

Promise 的 3 种状态,pendingfulfilledrejected源码如下

// Promise constants
extern enum PromiseState extends int31 constexpr 'Promise::PromiseState' {
  kPending,// 等待状态
  kFulfilled,// 成功状态
  kRejected// 失败状态
}

一个新创建的 Promise 处于 pending 状态。当调用 resolvereject 函数后,Promise 处于 fulfilledrejected 状态,此后 Promise 的状态保持不变,也就是说 Promise 的状态改变是不可逆的,如果再次调用 resolve 或者 reject 将不会发生任何事,Promise 源码中出现了多处状态相关的 assert(断言),这个就不赘述了,想必对于 Promise 的三种状态大家都比较熟悉了。

JSPromise

JSPromise 描述 Promise 的基本信息,源码如下

bitfield struct JSPromiseFlags extends uint31 {
  // Promise 的状态,kPending/kFulfilled/kRejected
  status: PromiseState: 2 bit; 
  // 是否有onFulfilled/onRejected处理函数,
  // 没有调用过 then 方法的 Promise 没有处理函数
  //(catch方法的本质是then方法,后面会介绍)
  has_handler: bool: 1 bit; 
  handled_hint: bool: 1 bit; 
  async_task_id: int32: 22 bit;
}

@generateCppClass
extern class JSPromise extends JSObject {
  macro Status(): PromiseState {
    // 获取 Promise 的状态,返回 
    // kPending/kFulfilled/kRejected 中的一个
    return this.flags.status;
  }

  macro SetStatus(status: constexpr PromiseState): void {
    // 只有 pending 状态的 Promise 才可以被改变状态
    assert(this.Status() == PromiseState::kPending);
    // Promise 创建成功后,不可将 Promise 设置为 pending 状态
    assert(status != PromiseState::kPending);
    this.flags.status = status;
  }

  macro HasHandler(): bool {
    // 判断 Promise 是否有处理函数
    return this.flags.has_handler;
  }

  macro SetHasHandler(): void {
    this.flags.has_handler = true;
  }

  // promise 处理函数或结果,可以是:
  // 空
  // onFulfilled/onRejected构成的链表
  // promise的确认值(resolve的参数)
  reactions_or_result: Zero|PromiseReaction|JSAny;
  flags: SmiTagged<JSPromiseFlags>;
}

Promise 状态改变时,比如调用了 resolve/reject 函数,SetStatus 方法会被调用;Javascript 层调用 resolve 方法时,reactions_or_result 字段会被赋值为 resolve 传入的参数;Javascript 层调用 then 方法时,说明已经有了处理函数,SetHasHandler() 会被调用。Status/SetStatus 这两个方法一个获取 Promise 状态,一个设置 Promise 状态;

其它

  • executor:是一个函数,Promise 构造函数接收的参数,调用 executor 时传入的参数分别是 resolvereject
  • PromiseReaction:是对象,表示 Promise 的处理函数,因为一个 Promise 多次调用 then 方法就会有多个处理函数,所以底层数据结构是个链表,每一个节点都存储着 onFulfilledonRejected 函数。
let p = new Promise((resolve, reject) => {
  resolve(123)
  // 会将 reactions_or_result 设置为 123
  // 会调用 SetHasHandler
  resolve(234)// 不会发生任何事,相当于没写
  reject(234)// 也不会发生任何事,相当于没写
})

构造函数

构造函数源码如下

PromiseConstructor(
    js-implicit context: NativeContext, receiver: JSAny,
    newTarget: JSAny)(executor: JSAny): JSAny {
  // 1. 如果不存在 new 关键字, throw a TypeError exception.
  if (newTarget == Undefined) {
    ThrowTypeError(MessageTemplate::kNotAPromise, newTarget);
  }

  // 2. 如果传入的参数不是一个回调函数, throw a TypeError exception.
  if (!Is<Callable>(executor)) {
    ThrowTypeError(MessageTemplate::kResolverNotAFunction, executor);
  }

  let result: JSPromise;
  // 构造一个 Promise 对象
  result = NewJSPromise();
  // 从 Promise 对象身上,获取它的 resolve 和 reject 函数
  const funcs = CreatePromiseResolvingFunctions(result, True, context);
  const resolve = funcs.resolve;
  const reject = funcs.reject;
  try {
    // 直接同步调用 executor 函数,resolve 和 reject 做为参数
    Call(context, UnsafeCast<Callable>(executor), Undefined, resolve, reject);
  } catch (e) {
    // 如果出现异常则调用 reject 函数
    Call(context, reject, Undefined, e);
  }
  return result;
}

首先分析两个 ThrowTypeError,以下代码可触发第一个 ThrowTypeError

Promise()  // Uncaught TypeError: undefined is not a promise

原因是没有使用 new 操作符调用 Promise 构造函数,此时 newTarget 等于 Undefined,触发了 ThrowTypeError(MessageTemplate::kNotAPromise, newTarget)

以下代码可触发第二个 ThrowTypeError

new Promise() // Uncaught TypeError: Promise resolver undefined is not a function

此时 newTarget 不等于 Undefined,不会触发第一个 ThrowTypeError。但调用 Promise 构造函数时没传参数 executor,触发了第二个 ThrowTypeError

executor 的类型是函数,在 JavaScript 的世界里,回调函数通常是异步调用,但 executor 是同步调用。在 Call(context, UnsafeCast(executor), Undefined, resolve, reject) 这一行,同步调用了 executor

console.log('同步执行开始')
new Promise((resolve, reject) => {
  resolve()
  console.log('executor 同步执行')
})

console.log('同步执行结束')
// 本段代码的打印顺序是:
// 同步执行开始
// executor 同步执行
// 同步执行结束

Promise 构造函数接收的参数 executor,是被同步调用的

then

ECMAScript 规范

PromisePrototypeThen

Promisethen 方法传入两个回调函数 onFulfilledonRejected,分别用于处理 fulfilledrejected 状态,并返回一个新的 Promise

JavaScript 层的 then 函数实际上是 V8 中的 PromisePrototypeThen 函数,源码如下

PromisePrototypeThen(js-implicit context: NativeContext, receiver: JSAny)(
    onFulfilled: JSAny, onRejected: JSAny): JSAny {
  const promise = Cast<JSPromise>(receiver) otherwise ThrowTypeError(
      MessageTemplate::kIncompatibleMethodReceiver, 'Promise.prototype.then',
      receiver);

  const promiseFun = UnsafeCast<JSFunction>(
      context[NativeContextSlot::PROMISE_FUNCTION_INDEX]);

  let resultPromiseOrCapability: JSPromise|PromiseCapability;
  let resultPromise: JSAny;
  label AllocateAndInit {
    // 创建一个新的 promise 用于当做本次 then 的调用结果返回
    //(上面有提到then的返回值是一个promise)
    const resultJSPromise = NewJSPromise(promise);
    resultPromiseOrCapability = resultJSPromise;
    resultPromise = resultJSPromise;
  }
  // onFulfilled 和 onRejected 是 then 接收的两个参数
  // 如果不传则默认值为 Undefined
  const onFulfilled = CastOrDefault<Callable>(onFulfilled, Undefined);
  const onRejected = CastOrDefault<Callable>(onRejected, Undefined);

  // 调用 PerformPromiseThenImpl 函数
  PerformPromiseThenImpl(
      promise, onFulfilled, onRejected, resultPromiseOrCapability);
  // 返回一个新的 Promise
  return resultPromise;
}

PromisePrototypeThen 函数创建了一个新的 Promise 对象,获取 then 接收到的两个参数,调用 PerformPromiseThenImpl 完成大部分工作。这里有一点值得注意,then 方法返回的是一个新创建的 Promise

const myPromise2 = new Promise((resolve, reject) => {
  resolve('foo')
})

const myPromise3 = myPromise2.then(console.log)

// myPromise2 和 myPromise3 是两个不同的对象
// 有不同的状态和不同的处理函数
console.log(myPromise2 === myPromise3) // 打印 false

then 方法返回的是一个新的 Promise

PerformPromiseThenImpl

ECMAScript 规范

PerformPromiseThenImpl 有4个参数,因为 PerformPromiseThenImpl 是在调用 then 调用,所以它的前三个参数分别是被调用 then 方法的 Promise 对象,以及这个 Promise 对象即将被绑定的两个处理函数 onFulfilledonRejected(值就是调用 then(onFulfilled, onRejected) 时传递的两个参数),最后一个参数为调用这个 then 返回的新 Promise 对象 resultPromiseOrCapability

PerformPromiseThenImpl 源码如下

transitioning macro PerformPromiseThenImpl(implicit context: Context)(
    promise: JSPromise, 
  	onFulfilled: Callable|Undefined,
    onRejected: Callable|Undefined,
    resultPromiseOrCapability: JSPromise|PromiseCapability|Undefined): void {
  if (promise.Status() == PromiseState::kPending) {
    // pending 状态的分支
    // 如果当前 Promise 还是 pending 状态
    // 那么只需要将本次 then 绑定的处理函数存储起来即可
    const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
    // 拿到 Promise 的 reactions_or_result 字段
    const promiseReactions =
        UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
    // 考虑一个 Promise 可能会有多个 then 的情况
    // reaction 是个链表,每次绑定处理函数都在链表的头部插入
    // 存 Promise 的所有处理函数
    const reaction = NewPromiseReaction(
        handlerContext, promiseReactions, resultPromiseOrCapability,
        onFulfilled, onRejected);
    // reactions_or_result 可以存 Promise 的处理函数的链表,也可以存
    // Promise 的最终结果,因为现在 Promise 处于 pending 状态,
    // 所以存的是处理函数 reaction 构成的链表
    promise.reactions_or_result = reaction;
  } else {
    // fulfilled 和 rejected 状态的分支
    const reactionsOrResult = promise.reactions_or_result;
    let microtask: PromiseReactionJobTask;
    let handlerContext: Context;
    // fulfilled 分支
    if (promise.Status() == PromiseState::kFulfilled) {
      handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
      // 生成 microtask 任务
      microtask = NewPromiseFulfillReactionJobTask(
          handlerContext, reactionsOrResult, onFulfilled,
          resultPromiseOrCapability);
    } else // rejected 分支
      deferred {
        assert(promise.Status() == PromiseState::kRejected);
        handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
      	// 生成 microtask 任务
        microtask = NewPromiseRejectReactionJobTask(
            handlerContext, reactionsOrResult, onRejected,
            resultPromiseOrCapability);
      	// 如果当前 promise 还未绑定过处理函数
        if (!promise.HasHandler()) {
          // 规范中的 HostPromiseRejectionTracker(promise, "reject"),
          // 作用是产生一个检测的 microtask 任务,后面会单独介绍。
          runtime::PromiseRevokeReject(promise);
        }
      }
    // 即使调用 then 方法时 promise 已经处于 fulfilled 或 rejected 状态,
    // then 方法的 onFulfilled 或 onRejected 参数也不会立刻执行,
    // 而是进入 microtask 队列后执行
    EnqueueMicrotask(handlerContext, microtask);
  }
  promise.SetHasHandler();
}

PerformPromiseThenImpl 函数的 pending 分支

PerformPromiseThenImpl 有三个分支,分别对应 Promise 的三个状态,当被调用 then 方法的 Promise 处于 pending 状态时则进入 pending分支。pending 分支调用 NewPromiseReaction 函数,在接收到的 onFulfilled 和 onRejected 参数的基础上,生成 PromiseReaction 对象,存储 Promise 的处理函数,并赋值给 JSPromisereactions_or_result 字段,然后调用 promise.SetHasHandler()has_handler 设置为 true(表示这个 Promise 对象已经绑定了处理函数)

考虑一个 Promise 可以会连续调用多个 then 的情况,比如:

const p = new Promise((resolve, reject) => {
  setTimeout(_ => {
    resolve('my code delay 2000 ms') 
  }, 2000)
})

p.then(result => {
  console.log('第 1 个 then')
})

p.then(result => {
  console.log('第 2 个 then')
})

p 调用了两次 then 方法,每个 then 方法都会生成一个 PromiseReaction 对象。第一次调用 then 方法时生成对象 PromiseReaction1,此时 p 的 reactions_or_result 存的是 PromiseReaction1。

第二次调用 then 方法时生成对象 PromiseReaction2,调用 NewPromiseReaction 函数时,PromiseReaction2.next = PromiseReaction1,PromiseReaction1 变成了 PromiseReaction2 的下一个节点,最后 p 的 reactions_or_result 存的是 PromiseReaction2。PromiseReaction2 后进入 Promise 处理函数的链表,却是链表的头结点。NewPromiseReaction 函数源码如下

macro NewPromiseReaction(implicit context: Context)(
    handlerContext: Context, next: Zero|PromiseReaction,
    promiseOrCapability: JSPromise|PromiseCapability|Undefined,
    fulfillHandler: Callable|Undefined,
    rejectHandler: Callable|Undefined): PromiseReaction {
  const nativeContext = LoadNativeContext(handlerContext);
  return new PromiseReaction{
    map: PromiseReactionMapConstant(),
    next: next, // next 字段存的是链表中的下一个节点
    reject_handler: rejectHandler,// 失败处理函数
    fulfill_handler: fulfillHandler,// 成功处理函数
    promise_or_capability: promiseOrCapability,// 产生的新Promise对象
    continuation_preserved_embedder_data: nativeContext
        [NativeContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX]
  };
}

在 p 处于 pending 状态时,p 的 reactions_or_result 字段大致内容如下图。

下图不是 microtask 队列,下图不是 microtask 队列,下图不是 microtask 队列。

reactions

图中使用 onFulfilled 代替 fulfill_handler 是为了方便理解,onRejected也是如此,且只包含于当前内容相关的字段,不用太过于纠结。

PerformPromiseThenImpl 函数的 fulfilled 分支

fulfilled 分支逻辑则简单的多,处理的是当 Promise 处于 fulfilled 状态时,调用 then 方法的逻辑:

先调用 NewPromiseFulfillReactionJobTask 生成 microtask,然后 EnqueueMicrotask(handlerContext, microtask) 将刚才生成的 microtask 放入 microtask 队列,最后调用 promise.SetHasHandler()has_handler 设置为 true

new Promise((resolve, reject) => {
  resolve()
}).then(result => {
  console.log('进入 microtask 队列后执行')
})

console.log('同步执行结束')
// 本段代码的打印顺序是:
// 同步执行结束
// 进入 microtask 队列后执行

尽管调用 then 方法时,Promise 已经处于 fulfilled 状态,但 then 方法的 onFulfilled 回调函数不会立即执行,而是进入 microtask 队列等待执行。

PerformPromiseThenImpl 函数的 rejected 分支

rejected 分支逻辑与 fulfilled 分支的逻辑大致相同,但是 rejected 分支中将 onRejected 处理函数加入 microtask 队列之前,会先判断当前 promise 是否已经存在处理函数,如果已经存在则会先调用 runtime::PromiseRevokeReject(promise),最后调用 promise.SetHasHandler()has_handler 设置为 true

if (!promise.HasHandler()) {
       runtime::PromiseRevokeReject(promise);
   }

这里的runtime::PromiseRevokeReject(promise) 就是 ECMAScript 规范 中的 HostPromiseRejectionTracker(promise, "handle")HostPromiseRejectionTracker 是一个抽象方法,这表示没有规定它的具体的逻辑。大致的作用是标记一下 promise 已经绑定了 rejected 状态的处理函数。不用疑惑为什么要这么做,后面会单独重点说。

注意 1 HostPromiseRejectionTracker 在两种情况下被调用:

当一个 promise 在没有任何处理程序的情况下被拒绝时,它的操作参数设置为“reject”。

当第一次将处理程序添加到被拒绝的 Promise 中时,将调用它并将其操作参数设置为“handle”。

引至 ——ECMAScript 规范(作者翻译)

小结

  1. 当一个 Promise 被调用 then 方法时,会创建一个新的 Promise 对象 resultPromise

  2. 然后根据当前 promise 的不同状态执行不同的逻辑

    • pending状态:会将 then 传递的两个处理函数变成一个 PromiseReaction 节点插入到promise.reactions_or_result 头部(PromiseReaction是一个链表结构),这个步骤就是在搜集依赖,等待 promise 状态完成时再触发。
    • fulfilled状态:会创建一个 microtask 来调用传入的 onFulfilled 处理函数,并将 reactions_or_result 作为调用的参数(此时 reactions_or_resultpromise 的值,也就是调用 resolve 时传入的参数 value ),并将其插入 microtask 队列。
    • rejected状态:与 fulfilled 状态类似,会将创建一个 microtask 来调用传入的 onRejected 处理函数,并将 reactions_or_result 作为调用的参数,如果当前 Promise 不存在处理函数(也就是 fulfilled 状态的Promsie 首次被调用 then 方法),会将其标记为已经绑定 onRejected 函数,然后将其 microtask 插入 microtask 队列。
  3. 调用 promise.SetHasHandler() 将 Promise 的 has_handler 设置为 true,表示其被调用的 then 方法绑定了处理函数。

  4. 最后返回新的 Promise 对象。

再来回顾一下 reactions_or_result 的3个值状态(空、链表、promise的值):

当 promise 刚刚被创建时,reactions_or_result的值的空,

当promise的状态改变为 fulfilled/rejected 时,其值是调用对应 resolve(value)/reject(value) 函数传入的参数 value,也就是 promise 的值。

当 promise 为 pending 状态且被调用 then 后,reactions_or_result 为一个链表,链表的每一项存储的是调用 then 时传入的处理函数。

reslove

new Promise((resolve, reject) => {
  setTimeout(_ => resolve('fulfilled'), 5000)
}).then(value => {
  console.log(value)
}, reason => {
  console.log('rejected')
})

上述代码 5s 后执行 resolve 函数,控制台打印 fulfilled。

FulfillPromise

ECMAScript 规范

reslove(value) 就是规范中的 FulfillPromise(promise, value) 函数,他的作用是将一个 Promise 的状态由 pending 改变为 fulfilled,并且将这个 Promise 的所有处理函数都变成 microtask 加入到 microtask 队列中等待执行。

resolve 函数归根到底调用了 V8 的 FulfillPromise 函数,源码如下

// https://tc39.es/ecma262/#sec-fulfillpromise
transitioning builtin
FulfillPromise(implicit context: Context)(
    promise: JSPromise, value: JSAny): Undefined {
  // 断案当前promise状态一定是 pending,因为promise 的状态改变是不可逆的
  assert(promise.Status() == PromiseState::kPending);

  // 取 Promise 的处理函数,在这之前 Promise 的状态还是 pending
  // 所以 reactions_or_result 中存的是 reactions 链表,
  // reactions 节点中存储的是里函数
  const reactions =
      UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);

  // Promise 需要修改为 fulfilled 状态,所以 reactions_or_result 存储的
  // 不再是处理函数,而是 Promise 的结果,也就是调用 resolve 时传入的参数
  promise.reactions_or_result = value;

  // 设置 Promise 的状态为 fulfilled
  promise.SetStatus(PromiseState::kFulfilled);

  // Promise 的处理函数,Promise 的结果都拿到了,开始正式处理
  TriggerPromiseReactions(reactions, value, kPromiseReactionFulfill);
  return Undefined;
}

FulfillPromise 的逻辑是获取 Promise 的处理函数到 reactionsreactions 的类型是 PromiseReaction,是个链表,忘记的同学可以回看上面的那张链表图片;设置 promisereactions_or_resultvalue,这个 value 就是 JavaScript 层传给 resolve 的参数;调用 promise.SetStatus(PromiseState::kFulfilled) 设置 promise 的状态为 fulfilled,最后调用 TriggerPromiseReactions 来将 reactions 中的处理函数添加到 microtask 队列。

TriggerPromiseReactions

源码如下

// https://tc39.es/ecma262/#sec-triggerpromisereactions
transitioning macro TriggerPromiseReactions(implicit context: Context)(
    reactions: Zero|PromiseReaction, argument: JSAny,
    reactionType: constexpr PromiseReactionType): void {
  // We need to reverse the {reactions} here, since we record them on the
  // JSPromise in the reverse order.
  let current = reactions;
  let reversed: Zero|PromiseReaction = kZero;
  // 链表反转
  while (true) {
    typeswitch (current) {
      case (Zero): {
        break;
      }
      case (currentReaction: PromiseReaction): {
        current = currentReaction.next;
        currentReaction.next = reversed;
        reversed = currentReaction;
      }
    }
  }
  current = reversed;
  // 链表反转后,调用 MorphAndEnqueuePromiseReaction
  // 把链接中的每一项都进入 microtask 队列
  while (true) {
    typeswitch (current) {
      case (Zero): {
        break;
      }
      case (currentReaction: PromiseReaction): {
        current = currentReaction.next;
        MorphAndEnqueuePromiseReaction(currentReaction, argument, reactionType);
      }
    }
  }
}

TriggerPromiseReactions 做了两件事:

  • 反转 reactions 链表,前文有分析过 then 方法的实现,then 方法的参数最终存在链表中。最后被调用的 then 方法,它接收的参数被包装后会位于链表的头部,这不符合规范,所以需要反转
  • 遍历 reactions 对象,调用 MorphAndEnqueuePromiseReaction 将每个元素放入 microtask 队列

MorphAndEnqueuePromiseReaction

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

MorphAndEnqueuePromiseReaction 接收 3 个参数,PromiseReaction 是前面提到的包装了 Promise 处理函数的链表对象,argument 是 resolve/reject 的参数,reactionType 表示 Promise 最终的状态,fulfilled 状态对应的值是 kPromiseReactionFulfill,rejected 状态对应的值是 kPromiseReactionReject。

MorphAndEnqueuePromiseReaction 的逻辑很简单,因为此时已经知道了 Promise 的最终状态,所以可以从 promiseReaction 对象得到 promiseReactionJobTask 对象,promiseReactionJobTask 的变量命名与 ECMAScript 规范相关描述一脉相承,其实就是传说中的 microtask。MorphAndEnqueuePromiseReaction 源码如下,仅保留了和本小节相关的内容。

transitioning macro MorphAndEnqueuePromiseReaction(implicit context: Context)(
    promiseReaction: PromiseReaction, argument: JSAny,
    reactionType: constexpr PromiseReactionType): void {
  let primaryHandler: Callable|Undefined;
  let secondaryHandler: Callable|Undefined;
  // 根据不同的 Promise 状态选取不同的回调执行
  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) {// fulfilled 分支
    * UnsafeConstCast(& promiseReaction.map) =
        PromiseFulfillReactionJobTaskMapConstant();
    const promiseReactionJobTask =
        UnsafeCast<PromiseFulfillReactionJobTask>(promiseReaction);
    // argument 是 reject 的参数
    promiseReactionJobTask.argument = argument;
    // handler 是 JS 层面 then 方法的第二个参数,或 catch 方法的参数
    promiseReactionJobTask.context = handlerContext;
    // promiseReactionJobTask 就是那个工作中经常被反复提起的 microtask
    // EnqueueMicrotask 将 microtask 插入 microtask 队列
    EnqueueMicrotask(handlerContext, promiseReactionJobTask);
    // 删除
  } else {// rejected 分支
      // 逻辑与 fulfilled 分支前面一致
    * UnsafeConstCast(& promiseReaction.map) =
        PromiseRejectReactionJobTaskMapConstant();
    const promiseReactionJobTask =
        UnsafeCast<PromiseRejectReactionJobTask>(promiseReaction);
    promiseReactionJobTask.argument = argument;
    promiseReactionJobTask.context = handlerContext;
    promiseReactionJobTask.handler = primaryHandler;
    EnqueueMicrotask(handlerContext, promiseReactionJobTask);
  }
}

MorphAndEnqueuePromiseReaction 的功能很简单,就是根据 Promise 的状态选取 onFulfilled 还是 onRejected 放到 microtask 队列准备执行。这里走的是 fulfilled 分支,所以选取的是 onFulfilled。

const myPromise4 = new Promise((resolve, reject) => {
  setTimeout(_ => {
    resolve('my code delay 1000') 
  }, 1000)
})

myPromise4.then(result => {
  console.log('第 1 个 then')
})

myPromise4.then(result => {
  console.log('第 2 个 then')
})
// 打印顺序:
// 第 1 个 then
// 第 2 个 then
// 如果把 TriggerPromiseReactions 中链表反转的代码注释掉,打印顺序为
// 第 2 个 then
// 第 1 个 then

小结

resolve 只会处理状态为 pending 的 Promise,会将 Promise 的 reactions_or_result 设置为传入的 value,用来作为 Promise 的值,并且会将 Promise 的状态修改为 fulfilled。

在调用 resolve 之前 reactions_or_result 其实是一个链表,存储的是当前 Promise 的所有处理函数,因为 promise 在使用 then 收集依赖时是将最新的依赖存放到链表头部,所以还需要先对链表进行反转,然后将其挨个放入 microtask 队列中等待执行

resolve 的主要工作是遍历上节调用 then 方法时收集到的依赖,放入 microtask 队列中等待执行。

reject

reject 与 reslove 没什么太大差别

ECMAScript 规范

new Promise((resolve, reject) => {
  setTimeout(_ => reject('rejected'), 5000)
}).then(_ => {
  console.log('fulfilled')
}, reason => {
  console.log(reason)
})

上述代码 5s 后执行 reject 函数,控制台打印 rejected。

RejectPromise

ECMAScript 规范

reject(season) 函数调用了 V8 的 RejectPromise(promise, season) 函数,源码如下

// https://tc39.es/ecma262/#sec-rejectpromise
transitioning builtin
RejectPromise(implicit context: Context)(
    promise: JSPromise, reason: JSAny, debugEvent: Boolean): JSAny {
	
  // 如果当前 Promise 没有绑定处理函数,
  // 则会调用 runtime::RejectPromise
  if (IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() ||
      !promise.HasHandler()) {
    return runtime::RejectPromise(promise, reason, debugEvent);
  }
 
  // 取出 Promise 的处理对象 PromiseReaction
  const reactions =
      UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
  // 这里的 reason 就是 reject 函数的参数
  promise.reactions_or_result = reason;
  // 设置 Promise 的状态为 rejected
  promise.SetStatus(PromiseState::kRejected);
  // 将 Promise 的处理函数都添加到 microtask 队列
  TriggerPromiseReactions(reactions, reason, kPromiseReactionReject);
  return Undefined;
}

HostPromiseRejectionTracker

与 ReslovePromise 相比,RejectPromise 中多出一个判断 Promsie 是否绑定了处理函数的判断,如果没有绑定处理函数则会先执行 runtime::RejectPromise(promise, reason, debugEvent),这是其实是 ECMAScript 规范中的 HostPromiseRejectionTracker(promise, "reject") ,这已经是第二次提到 HostPromiseRejectionTracker了。

PerformPromiseThenImpl 函数的 rejected 分支 处有提到过一次。

在 ECMAScript 规范中 HostPromiseRejectionTracker 是一个抽象方法,他甚至没有明确的执行过程,好在规范中描述了他的作用。

HostPromiseRejectionTracker 用于跟踪 Promise 的 rejected,例如全局的 rejectionHandled 事件就是由它实现。

注1 HostPromiseRejectionTracker 在两种情况下被调用:

当一个 Promise 在没有任何处理函数的情况下被调用 reject 时,调用它并且第二个参数传递 “reject”。

当第一次为 rejected 状态的 Promise 绑定处理函数时,调用它并且第二个参数传递 “handle”。

所以在这里,当传递 “handle” 就相对于为这个 Promise 对象标记为已经绑定了处理函数,当传递 “reject” 相对于标记这个 Promise 对象还没有处理函数。

我们先来看几段代码看看他的实际作用

当我们调用一个 Promise 的状态为 reject 且未为其绑定 onRejected 的处理函数时, JavaScript会抛出错误

const myPromise1 = new Promise((resolve, reject) => {
    reject()
})
// 报错

并且检测是否绑定处理函数是一个异步的过程

console.log(1);
const myPromise1 = new Promise((resolve, reject) => {
    reject()
})
console.log(2);
// 1
// 2
// 报错

我们可以为其绑定一个 onRejected 处理函数来解决我们报错

const myPromise1 = new Promise((resolve, reject) => {
    reject()
})// 得到一个 rejected 状态的 Promise
myPromise1.then(undefined, console.log)

你一定会疑惑,Promise 是在何时检测它是否绑定了 onRejected 处理函数,如何检测的?

这就是 HostPromiseRejectionTracker 的作用,在 ECMAScript 规范中还提到,当调用 HostPromiseRejectionTracker(promise, 'reject') 时, 如果 promsie 不存在处理函数,则会为其设置一个处理函数。

回到上面的逻辑,当一个 Promise 的 reject 函数被调用时, 如果没有 onRejected 处理函数,则会调用 runtime::RejectPromise 来为其添加一个处理函数,然后后面会调用 TriggerPromiseReactions 将这个处理函数加入到 microtask 队列,这个处理函数执行时做的事情就是再次检测 Promise 是否被绑定了新的 onRejected(也就是有没有在此期间执行了 HostPromiseRejectionTracker(promise, 'handle') ),如果没有则抛出错误,如果有则什么也不发生。

// 不可运行的js伪代码
function HostPromiseRejectionTracker(promise, status) {
  if (status === 'handle') {
    promise.HasHandler = true
  } else if (status === 'reject'){
    promise.catch(() => {
      if (!promise.HasHandler) {
        throw new Error('Uncaught (in promise) ' + promise.value)
      }
    })
  }
}

RejectPromise(){
  //...
  if (!promise.HasHandler) {
    HostPromiseRejectionTracker(promise, 'reject')
  }
  //...
}

FulfillPromise(){
  //...
  if (!promise.HasHandler) {
    HostPromiseRejectionTracker(promise, 'handle')
  }
  //...
}

所以在对一个 reject 状态的 Promise 调用 then 方法时需要对其调用 runtime::PromiseRevokeReject(promise) 来表示这个 Promise 绑定了新的 onRejected,防止错误被抛出。

所以你必须要赶在这个检测的 microtask 执行之前绑定处理函数才能防止这个错误的抛出。

const myPromise1 = new Promise((resolve, reject) => {
    // 同步执行
    reject()
    // 会向 microtask 队列中插入一个检查 myPromise1 
    // 是否绑定了新的 onRejected 处理函数的 microtask
})

// macrotask
setTimeout(() => {
  	// 此时 microtask 已经执行,错误已经抛出,来不及了
    myPromise1.then(undefined, console.log)
}, 0)

注意:  浏览器控制台有一个非常奇怪的特性,如果在这个错误输出后在为其绑定 onrejected 处理函数,浏览器会将控制台的错误覆盖掉。所以如果你在浏览器执行这段代码,请将setTimeout的时间设置长一点,这样效果更加容易肉眼可见,或者切换到 node 环境中来运行。

小结

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

  • 设置 Promise 的 reason,也就是 reject 的参数
  • 设置 Promise 的状态:rejected
  • 如果 Promise 没有 onRejected 处理函数,则会为其添加一个再次检测 Promise 是否绑定 onRejected 的处理函数,这个处理函数会被放入 microtask 队列,如果其执行时 Promise 还未绑定 onRejected,则会抛出一个错误。
  • 由之前调用 then/catch 方法时收集到的依赖存储到 reactions_or_result 的处理函数,也就是许多 promiseReaction 节点对象,得到一个个 microtask,最后将这些 microtask 插入 microtask 队列

catch

new Promise((resolve, reject) => {
    setTimeout(reject, 2000)
}).catch(_ => {
    console.log('rejected')
})

PromisePrototypeCatch

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

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));
}

PromisePrototypeCatch 的源码确实只有就这几行,除了调用 InvokeThen 方法再无其它 。

InvokeThen

从名字可以推测出,InvokeThen 调用的是 Promise 的 then 方法,InvokeThen 源码如下:

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));
      <