likes
comments
collection
share

这就是编程:V8 Promise是如何承诺我们

作者站长头像
站长
· 阅读数 115

前言

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

下面这段话将作为该系列的slogan:

“《这就是编程》,每周六22:00定期更新,让我们如期打开编程世界的大门。借助源码,让我们进入编程宇宙,自由翱翔、尽情探索其中奥秘,让我们一起读懂源码,读懂编程,我相信这其中有着无限乐趣与惊喜。”

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

Let's start!

初识Promise

JSPromise在cxx里结构是什么: 

这就是编程:V8 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执行,

这就是编程:V8 Promise是如何承诺我们

  1. 创建JSPromise实例
  2. 创建PromiseResolvingFunctions,也就是resolve/reject方法对,它们是两个built-in方法,也是JSPromis实例状态变更的唯一触发点,可谓Promise核心机制之一
  3. 调用executor(resolve, reject),通过try-catch进行捕获并直接进入rejected状态,所以executor里抛错同样能被onRejected handler处理
  4. 最后返回JSPromise实例 

注意:

在Promise构建过程中executor是同步执行

绑定回调

得到JSPromise实例后,就一定会调用其then方法,不然JSPromise实例就失去了意义。

js层面Promise.prototype.then是一个built-in方法,对应cxx层面的PromisePrototypeThen

这就是编程:V8 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链表里保存会每一次then方法调用的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实例

这就是编程:V8 Promise是如何承诺我们

针对then方法调用后,handler先绑定先执行,但上图反映的PromiseReactions链表其实顺序刚好相反,先埋个伏笔。

OK,接着看,其实then方法核心逻辑在PerformPromiseThenImpl

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

这就是编程:V8 Promise是如何承诺我们

注意:

  1. promise链式调用是基于新创建的JSPromise实例
  2. 如果JSPromise实例已经从pending状态变成fulfilled/rejected状态,意味着在调用then方法之前resolve/reject方法已经触发,这时在then方法里会直接创建PromiseReactionJobTask并进入microtask队列等待,对应PerformPromiseThenImpl里状态判断if-else的else分支,这里先按下不表,因为then和resolve/reject方法对之间的关系还没揭开,留到后面详细说明。不过可以先简单了解下cxx源码中会根据不同状态创建PromiseReactionJobTask,fulfilled时为NewPromiseFulfillReactionJobTask;rejected时为NewPromiseRejectReactionJobTask
  3. 由2得到,resolve/reject方法对都是可以单独同步调用,这从下文也能得到佐证,这也是deferred的实现基础

这里暂且先对PromiseReactionJobTask保持印象,这是microtask队列中一种关于Promise的JobTask,后面还会提到另一种。

OK,在promise状态为pending且经过then方法调用后,PromiseReactions已经创建并建立,这时resolve方法触发,表现是状态从pending进入fulfilled。

cxx层面的ResolvePromise方法是resolve方法的built-in实现。

这就是编程:V8 Promise是如何承诺我们

ResolvePromise接收两个入参:

  • resolve方法的上下文context所对应的JSPromise实例
  • 透传resolve方法的入参resolution

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

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

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

都是在microtask中关于Promise的JobTask。

注意:

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

同时抛一个题,代号叫“看似简单的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方法调用先绑定先执行,那不就输出:

0
4
1
2
3

答案对吗?你对handler的执行时机和方式真的了解了吗?

队列等待

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

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

这就是编程:V8 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:

reactionType根据resolve/reject方法确定,传递对应的fulfilled/rejected。

这就是编程:V8 Promise是如何承诺我们

  1. 因为上文提到PromiseReactions链表是反序的,所以要先反转重新生成顺序正确的PromiseReactions
  2. 遍历顺序正确的PromiseReactions,分别MorphAndEnqueuePromiseReaction

MorphAndEnqueuePromiseReaction逻辑简单清晰,针对当前promise状态分别创建对应的PromiseReactionJobTask并放入microtask队列等待。

注意:

此时PromiseReactionJobTask所存的argument属性值就是由resolve方法入参经FulfillPromise再经TriggerPromiseReactions一路透传,一直到MorphAndEnqueuePromiseReaction。

这就是编程:V8 Promise是如何承诺我们

如下图,PromiseReactions经过反转得到了最后handler正确顺序执行的结果。

这就是编程:V8 Promise是如何承诺我们

经过FulfillPromise,onFulfilled handler执行方式以及执行时机就非常清晰了:

  • 执行方式:所有的onFulfilled hanlder都是在microtask队列中异步执行,
  • 执行时机:只有在resolve方法调用后才开始被放入microtask队列等到

回过头再看“看似简单的promise”这道题,现在应该有不同答案了:

0
1
2
4
3

但是这答案对吗?🤔这里可是return Promise.resolve(4)呢?

注意:

对于promise的reject方法逻辑链路,它和resolve方法逻辑链路基本一致,由RejectPromise触发,将promise状态设置为rejected,而此时TriggerPromiseReactions传递的reactionType是rejected。

特别注意的是RejectPromise也同样会创建PromiseReactionJob,但是PromiseReactionJob中onRejected handler执行完照样会进入promise chain下一次的FuflfillPromiseReactionJob,这就是为什么reject触发之后,下一跳的onFulfilled handler会触发的原因所在。

异步执行

PromiseReactionJob

随着事件循环Event-Loop,microtask队列开始先进先出的执行所有的JobTask。上面提到对于Promise,有两种JobTask:

  • PromiseReactionJobTask,创建PromiseReactionJob
  • PromiseResolveThenableJobTask,创建PromiseResolveThenableJob

注意:

上文提到在promise不处于pending状态时,调用then方法会根据promise当前状态分别创建JobTask放入microtask队列:

  • fulfilled对应NewPromiseFulfillReactionJobTask,会创建PromiseFulfillReactionJob

  • rejected对应NewPromiseRejectReactionJobTask,会创建PromiseRejectReactionJob

底层调用的也还是PromiseReactionJob:

这就是编程:V8 Promise是如何承诺我们

归根结底,最后所有的handler处理逻辑都是在PromiseReactionJob中被触发:

这就是编程:V8 Promise是如何承诺我们

针对handler是否存在分成两个逻辑分支:

  1. handler存在,这是常规场景,将一路透传保存下来的resolve入参,传入handler执行
  2. handler执行成功,需对promise chain的下一跳PromiseCapacity进行“resolve”,而此时的入参就是handler的返回值,也就是调用PromiseCapacity.resolve,再次进入新一轮的ResolvePromise,只不过这次对应的JSPromise变成了PromiseCapacity.promise,就这样,promise chain逐次传递。
  3. 执行失败,则直接调用PromiseCapacity.reject,进入rejected状态
  4. handler不存在,先按下不表,这种情况只有在PromiseResolveThenableJob里存在,下面说

PromiseResolveThenableJob

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

但是假设handler的返回值是一个thenable对象会怎么样,意味着PromiseCapacity.resolve(thenable),意味着这时ResolvePromise接收到的resolution参数是一个thenable,此时内部逻辑分支会进入Enqueue:

这就是编程:V8 Promise是如何承诺我们

着重看一下NewPromiseResolveThenableJobTask的参数:

  • promise,当前resolve方法归属的promise,针对PromiseCapacity,这里promise就是PromiseCapacity.promise,命名为promiseToResolve,代表将要处理的promise实例

  • resolution,handler返回的thenable对象,带then方法的普通对象或者是一个JSPromise实例,称呼它为promise中间产物

  • then,这个thenable的then方法,callable的普通函数或者Promise.prototype.then

最后得到PromiseResolveThenableJobTask实例并同样被放入microtask队列等待:

这就是编程:V8 Promise是如何承诺我们

那这个临时生成的promise中间产物如何和当前的PromiseCapacity.promise产生关联,为什么thenable resolve之后PromiseCapacity.promise也会同样resolve?接着看,下面解答。

现在在microtask队里,轮到PromiseResolveThenableJob执行了:

这就是编程:V8 Promise是如何承诺我们

如果handler返回的thenable是一个JSPromise实例,则其then方法就是Promise.prototype.then,和context的then方法是相同

  1. 则针对thenable进行一次then方法调用,即PerformPromiseThenImpl,而promiseToResolve被作为这一次调用的临时PromiseCapability
  2. 而且此时的onFulfilled/onRejected handlers设置为空,handler为空是这次then方法调用的最鲜明特点,后续有重用
  3. 而等到这个中间产物resolve时,又是一次由ResolvePromise逻辑触发的PromiseReactionJob,此时由于handler不存在,直接进FuflfillPromiseReactionJob
  4. FuflfillPromiseReactionJob里,promise chain中的下一跳promiseToResolve就会进入ResolvePromise,然后又进入PromiseReactionJob,promise chain开始逐次传递

如果handler返回的thenable只是一个带有then方法的普通对象

  1. 则通过promiseToResolve创建resolve/reject方法对
  2. 并以这个thenable为上下文context调用普通对象的then方法,传入resolve/reject方法对,最终达到promiseToResolve.resolve的效果

如下面这个例子

Promise.resolve().then(() => {
    return {
        then(resolve, reject) {
            resolve(1)
        }
    }
}).then((res) => console.log(res));

// 1

注意:

现在弄明白当handler返回一个thenable对象时,会先在microtask队列里放入一个PromiseResolveThenableJobTask,然后经由PromiseResolveThenableJob又在microtask队列里放入PromiseReactionJobTask。那么现在回头看“看似简单的promise”这道题,正确答案应该是

0
1
2
3
4

因为return Promise.resolve(4)导致在microtask队列里多放入了一个PromiseResolveThenableJobTask,使得console.log(3)这个handler所在的PromiseReactionJobTask先于console.log(res),而microtask队列先进先出,所以先输出了3,后输出4

其他方法

Promise.resolve

Promise.resolve()返回的是一个创建完成就进入fulfilled状态的JSPromise实例。

这就是编程:V8 Promise是如何承诺我们

如果入参不是thenable,直接进入NeedToAllocate代码逻辑块处理,

  • 如果当前上下文中的Promise构造器为cxx原生Promise构造器,NewJSPromise生成promise实例并对其ResolvePromise,此时该该promise已经处于fulfilled状态,且值锁定为value
  • 通过当前上下文中的Promise构造器创建PromiseCapability并直接调用PromiseCapability.resolve(value),最后返回PromiseCapability.promise,此时该PromiseCapability.promise已经处于fulfilled状态,且值锁定为value

如果入参为thenable

  • 当该thenable为JSPromise实例,即它的构造器和cxx原生Promise构造器相同,直接返回这个JSPromise
  • 如果该thenable不是JSPromise实例,且不为cxx原生Promise的子类,进入NeedToAllocate代码逻辑块处理

后续所有处理就是针对这个返回的JSPromise实例,但是由于它的状态已经变成fulfilled,所以then方法调用时会直接创建PromiseReactionJobTask。PromiseReactionJobTask的执行上文已经分解过了。

比如下面这段代码:

Promise.resolve(1).then((res) => { console.log(res); })

.then((res) => { console.log(res); })这个then调用时直接将(res) => { console.log(res); }这个onFulfilled handler包装成PromiseReactionJobTask

Promise.reject

Promise.reject()返回一个创建完成并进入rejected状态的JSPromise实例。

这就是编程:V8 Promise是如何承诺我们

  • 对reject静态方法调用的上下文context如果不是cxx原生Promise构造器,NewPromiseCapability创建PromiseCapability并直接调用PromiseCapability.reject(reason),返回PromiseCapability.promise
  • 如果是Promise.reject的方式去调用,则由NewJSPromise(PromiseState::kRejected, reason)生成一个状态为rejected的JSPromise,并交由runtime::PromiseRejectEventFromStack(promise, reason)去处理,因为一个状态为rejected的JSPromise如果没有添加任何onRejected handler时,需要由runtime去帮助捕获错误

Promise.all

Promise.all入参可以接收一个iterable,该iterable迭代器的每一项可以是JSPromise/thenable/普通值。

这就是编程:V8 Promise是如何承诺我们

当通过Promise.all方式调用,当前all方法里的上下文receiver就是cxx原生Promise构造器,通过它进行NewPromiseCapability生成PromiseCapability,此时promiseResolveFunction即为built-in的resolve方法。

这就是编程:V8 Promise是如何承诺我们

进入PerformPromiseAll方法后,对接收到的迭代器iterator遍历处理,每一次迭代器遍历时:

  1. 获取迭代器next value

  2. 根据当前迭代器所处index,创建PromiseResolvingFunctions,并绑定对应的index到resolve/reject方法上。此处的resolve/reject方法对是游离存在的,但是都和同一个resolveElementContext关联

  3. 根据next value值的类型进行逻辑分支,这里稍稍和ResolvePromise里碰到的值类型判断有些许区别,大致可以理解为一次ResolvePromise(promise, nextValue)

  4. 如果next value不为thenable,直接通过Promise.resolve(nextValue)方式创建一个fulfilled状态的JSPromise实例,此时针对该JSPromise调用其then方法,传入上面创建的一对游离resolve/reject方法分别作为onFulfilled/onRejected handlers。则由于JSPromise实例状态已为fulfilled,则作为onFulfilled handler的resolve方法会被触发

  5. 当next value是一个thenable时,将对其进行PerformPromiseThenImpl,也就是调用thenable.then,而且此时创建的这对游离resolve/reject方法也会被作为onFulfilled/onRejected handlers传入,意味着resolve/reject方法会在PromiseReactionJob中被触发

  6. 当这些临时根据index创建的游离resolve/reject方法对被触发后,会将对应的结果值存入*NativeContextSlot(nativeContext, ContextSlot::JS_ARRAY_PACKED_ELEMENTS_MAP_INDEX)所对应的arrayMap,在js层面,简单理解就是存放入一个堆内存上。最后通过PromiseCapability.resolve来触发

注意:

在任意一次迭代器结果获取中出错,对应的PromiseCapability.promise都会进入rejected,所以针对于Promise.all,只有迭代器中所有结果值都resolve才能进入fulfilled状态。

结尾

经过以上分析,现在彻底明白Promise实现原理:

  • 基于microtask队列实现异步执行
  • 通过PromiseCapability的deferred模式,完美实现promise chain

在此基础上,Promise实现逻辑中关于把handler转化为PromiseReactionJobTask并放入microtask队列的巧思,既是对Event-Loop机制的保护,也是对js引擎单线程特点的充分利用。

那么,关于js异步调用的认识,你有哪些想法呢,欢迎留言讨论。

参考资料

转载自:https://juejin.cn/post/7201047960834588730
评论
请登录