这就是编程:你知道在cxx语言层面Promise如何实现吗

lxf2023-03-17 08:15:01

这就是编程:你知道在cxx语言层面Promise如何实现吗

前言

2022年,文章写少了,源码也看少了,进入2023年,就希望能坚持写作,把每一次源码阅读的过程都记录下来,最后收到《这就是编程》系列中。

“编程,就像一场开卷考试,题目的答案取决于对书本内容的熟悉程度;而一份源代码就好比一本书,正所谓读书百遍其义自见,读懂源码就读懂了编程!”

今天带来关于V8 Promise在cxx语言层面的源码分析,一文带你全面了解V8 Promise的实现原理及使用细节,让你对js异步编程有新的认识。

Let's start!

初识Promise

JSPromise在cxx里结构是什么: 

这就是编程:你知道在cxx语言层面Promise如何实现吗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执行,

这就是编程:你知道在cxx语言层面Promise如何实现吗

  1. 创建JSPromise实例
  2. 创建PromiseResolvingFunctions,也就是resolve/reject方法对,它们是两个built-in方法,也是JSPromis实例状态变更的唯一触发点,可谓Promise核心机制之一
  3. 调用executor(resolve, reject),而且通过try-catch对executor执行过程进行错误捕获
  4. 最后返回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。

这就是编程:你知道在cxx语言层面Promise如何实现吗

  1. 获取onFulfilled/onRejected handlers的执行上下文context
  2. 创建PromiseCapability,一个deferred的对象,既包含JSPromise实例,也包含对应的resolve/reject方法对,JSPromise实例总是和它的resolve/reject方法对搭配出现;同时PromiseCapability是promise chain的关键
  3. 针对刚刚创建的PromiseCapability,加上onFulfilled/onRejected handlers信息,创建PromiseReaction,PromiseReaction是后续handler被调用的载体,因为它是创建PromiseReactionJobTask的依据,然后配合当前JSPromise实例的reactions_or_result建立PromiseReactions链表
  4. 返回新创建的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实例

这就是编程:你知道在cxx语言层面Promise如何实现吗

then方法调用后,需要达到先绑定的handler先执行的特点,那么上图反映的PromiseReactions链表其实顺序刚好相反,笔者疑惑cxx层面为什么不在建立PromiseReactions链表时就按正确顺序呢?先埋个伏笔,看看后续cxx对这个顺序如何处理。

OK,接着往下,其实then方法核心逻辑在PerformPromiseThenImpl里。

当promise的状态处于pending时,then方法才会建立PromiseReactions链表。

这就是编程:你知道在cxx语言层面Promise如何实现吗

注意:

  1. promise链式调用是基于新创建的JSPromise实例
  2. 如果JSPromise实例已经从pending状态变成fulfilled/rejected状态,意味着在调用then方法之前resolve/reject方法已经触发,这时在then方法里会直接创建PromiseReactionJobTask并进入microtask队列等待,对应PerformPromiseThenImpl里状态判断if-else的else分支,这里先不展开,后文分解,不过可以先简单了解下cxx源码中会根据不同状态创建不同的PromiseReactionJobTask,状态为fulfilled时NewPromiseFulfillReactionJobTask,状态为rejected时NewPromiseRejectReactionJobTask
  3. 由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方法。

这就是编程:你知道在cxx语言层面Promise如何实现吗

ResolvePromise接收两个入参:

  • resolve方法的上下文context所对应的JSPromise实例
  • 作为promise.reactions_or_result值的resolution入参

在ResolvePromise里,主要是针对resolve方法入参类型进行分支判断:

  1. 非对象,直接FulfillPromise
  2. 对象但非thenable,直接FulfillPromise
  3. thenable,进入Enqueue,创建PromiseResolveThenableJobTask放入microtask队列等待,这里先不重点分析这种情况,下文详细说明 

这里出现的PromiseResolveThenableJobTask,和上面提到的PromiseReactionJobTask一样,

都是在microtask队列中关于Promise的JobTask。

注意:

目前onFulfilled handler都还没有执行,那么什么时候执行,什么方式执行,关键就在FulfillPromise里,它也是JSPromise实例进入fulfilled状态的唯一触发点。

队列等待

现在假设resolve入参为非thenable类型,即直接进入FulfillPromise的调用逻辑。

在FulfillPromise中,会将遍历PromiseReactions链表生成的一个个PromiseReactionJobTask放入microtask队列等待执行,这种PromiseReactionJobTask创建场景是最常见的,也就是说then方法调用在resolve方法之前。

这就是编程:你知道在cxx语言层面Promise如何实现吗

  1. 判断当前promise状态值是否处于pending,这点尤为重要,一旦promise状态发生变化意味着resolve/reject方法失去意义,即这个时候调用resolve/reject方法将不会有任何副作用
  2. 缓存当前JSPromise的reactions_or_result,这时的reactions_or_result为PromiseReactions链表,存放着promise chain信息
  3. 将reactions_or_result值设置为resolve入参,对应js层面就是设置promise.[[PromiseResult]];同时设置JSPromise状态值为fulfilled,对应js层面就是设置promise.[[PromiseState]]。这里也看出,先设置状态再触发PromiseReactions。
  4. TriggerPromiseReactions,遍历PromiseReactions分别创建PromiseReactionJobTask。

注意:

从PerformPromiseThenImpl可以看出,对已经不处于pending状态的promise多次调用then方法等价于往microtask队列里放入task,且handler接收到的入参值都是promise.reactions_or_result,因为这个值经由FulfillPromise逻辑处理已经锁定

再看TriggerPromiseReactions,将当前resolve入参透传到MorphAndEnqueuePromiseReaction,同时根据resolve/reject方法的调用设置reactionType值,分别传递fulfilled/rejected。

这就是编程:你知道在cxx语言层面Promise如何实现吗

  1. 因为上文提到PromiseReactions链表是反序的,所以要先反转重新生成顺序正确的PromiseReactions
  2. 遍历顺序正确的PromiseReactions,分别对每一个PromiseReaction进行MorphAndEnqueuePromiseReaction逻辑处理,就是根据当前promise状态,分别对其创建对应的PromiseReactionJobTask并放入microtask队列等待,由于microtask队列是先进先出,所以前面才要反转PromiseReactions链表,这样先绑定的handler才能先出栈执行。笔者认为cxx层面在建立PromiseReactions链表时完全可以一步到位直接建立一个顺序正确的链表,不知道cxx层面为什么没这么做?