【V8源码补充篇】从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

10,899 阅读9分钟

罪魁祸首还是先挂出来 👇

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

接上篇 👉 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

手写 Promise 完整代码 👉 github:my-promise

问题回顾

上篇我们通过从零手写 Promise 的方式,带着大家去深入了解了一下 Promise 的一些实现细节。

最后我们发现其实只需要创建一次微任务,就可以处理 then 方法内部 return Promise.resolve(4) 的问题,所以我们没有办法在手写 Promise 实现中去找到到合理的解释,只能通过一些概念进行了猜测。

没有真正窥探到原生 Promise 的内部实现逻辑,似乎让人感觉有点隔靴搔痒 🙈

虽然一次还是两次微任务对我们实际生产也没有什么实质影响,创建两次微任务的代码逻辑也可能会在后续的某次迭代中被改掉。

但是我们不能不面对新的疑问:

  • 原生 Promise 是不是真的产生了两次微任务来处理 return Promise.resolve(4)?
  • Promise V8 源码中有没有关键信息可以解释这个现象?

本着刨根问题对精神,还是决定做一下 Promise V8 源码内容补充,也算是对 V8 源码学习的一次启蒙。

Promise V8 源码如何阅读?

废话不多说,来开始看源码 👉 源码地址,注释内容来自我们ECMAScript® 2022 规范。

你可能会看的脑壳疼 😂 更糟糕的是 Promise 的实现逻辑是实际上分布在不同的代码块中的,直接吃生肉很容易消化不良。

所以这里先推荐两篇 Promise V8源码分析文章(PS:我尝过,是熟的):

在熟悉 Promise V8 源码的大致结构之后,再回到之前的问题 👇

原生 Promise 是不是真的产生了两次微任务?

在 Promise V8 源码中通过 RunSingleMicrotask 运行一个微任务。如果想要了解微任务的创建情况,就可以通过在RunSingleMicrotask 打印调用信息来观察。

我们来看一下在运行那道面试题时,RunSingleMicrotask 中打印的信息,图片来自知乎@徐鹏跃

v2-20dd94a382c3d43928dac616eb895498_720w.jpg

用红框圈出的信息,就是那两次神秘的微任务,所以我们这里我们就可以确认,确实是创建了两次微任务

Promise V8 源码中关键信息在哪里?

实际上我们通过上一篇的分析,我们知道有一次微任务创建的位置是很清晰的。那就是在发现 onFufilled 回调函数执行结果是一个 Promise 的时候,它会调用一次 then 方法去处理这种情况,调用 then 方法那就必然会使用 queueMicrotask 创建一次微任务。

先看一下面试题中这个 Promise

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

我们再来回顾一下上一篇中是如何处理 return Promise.resolve(4) 的 👇

  1. Promise.resolve() 执行,修改 Promise 状态为 fulfilled;
// 更改成功后的状态
resolve = (value) => {
  // 只有状态是等待,才执行状态修改
  if (this.status === PENDING) {
    // 状态修改为成功
    this.status = FULFILLED;
    // 保存成功之后的值
    this.value = value;
    // resolve里面将所有成功的回调拿出来执行
    while (this.onFulfilledCallbacks.length) {
      // Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空
      this.onFulfilledCallbacks.shift()(value)
    }
  }
}
  1. then 初始化的时候,在这之前 Promise.resolve() 已经修改状态为 fulfilled,所以这里会立即通过 queueMicrotask 创建微任务将 onFulfilled 回调函数送入微任务队列;
// onFulfilled 回调函数
onFulfilled = () => {
    console.log(0);
    return Promise.resolve(4);
}
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
  try {
    // 获取成功回调函数的执行结果
    const x = realOnFulfilled(this.value);
    // 传入 resolvePromise 集中处理
    resolvePromise(promise2, x, resolve, reject);
  } catch (error) {
    reject(error)
  } 
}) 
  1. 在 then 全部初始化完成后,同步代码执行结束,开始执行微任务列表中排队的任务,onFulfilled 回调函数此时会被调用,onFulfilled 函数的执行结果 x 会传入 resolvePromise 方法进行处理,此时 x 为 Promise.resolve(4) ;
// 获取成功回调函数的执行结果
const x = realOnFulfilled(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(promise2, x, resolve, reject);
  1. 判断返回值 x 的类型,如果 typeof x === object 或者 typeof x === function ,同时判断 x.then 存在,此时 x 为 Promise.resolve(4),符合上面的条件,则调用 then 方法(这里就会创建一次微任务),得到结果 y 继续调用 resolvePromise 递归判断,这里 y = 4,即不为 Promise, 调用 resolve(4) ,注意这里的 resolve 方法是外部 Promise 的,相当于将 Promise.resolve(4) 的执行状态与结果提供给外部的 Promise,完整代码是这样 👇
function resolvePromise(promise, x, resolve, reject) {
  // 如果相等了,说明return的是自己,抛出类型错误并返回
  if (promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  if (typeof x === 'object' || typeof x === 'function') {
    // x 为 null 直接返回,走后面的逻辑会报错
    if (x === null) {
      return resolve(x);
    }

    let then;
    try {
      // 把 x.then 赋值给 then 
      then = x.then;
    } catch (error) {
      // 如果取 x.then 的值时抛出错误 error ,则以 error 为据因拒绝 promise
      return reject(error);
    }

    // 如果 then 是函数
    if (typeof then === 'function') {
      let called = false;
      try {
        then.call(
          x, // this 指向 x
          // 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
          y => {
            // 如果 resolvePromise 和 rejectPromise 均被调用,
            // 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            // 实现这条需要前面加一个变量 called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果调用 then 方法抛出了异常 error:
        // 如果 resolvePromise 或 rejectPromise 已经被调用,直接返回
        if (called) return;

        // 否则以 error 为据因拒绝 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函数,以 x 为参数执行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
}

通过对手写 Promise 回顾,我们知道在处理 Promise.resolve(4)的时候,调用了 then 方法,来修改状态并拿到 Promise 的结果,这里也就创建了一次微任务。回过来我们再看一下在原生 Promise 中是怎么处理的。

实际上在 Promise V8 源码中也有类似上面的 resolvePromise 的处理,在 ResolvePromise 方法中 👇

// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning builtin
ResolvePromise(implicit context: Context)(
    promise: JSPromise, resolution: JSAny): JSAny {
  try {
    // 8. If Type(resolution) is not Object, then
    // 8.a Return FulfillPromise(promise, resolution).
    
    // 如果 resolution 是整数/字符串
    if (TaggedIsSmi(resolution)) {      
      // FulfillPromise 把 promise 状态变为 fulfilled 状态
      return FulfillPromise(promise, resolution);
    }
    const promisePrototype =
        *NativeContextSlot(ContextSlot::PROMISE_PROTOTYPE_INDEX);
        
    // 判断 resolution 的类型是否为 Promise
    if (resolutionMap.prototype == promisePrototype) {
      // The {resolution} is a native Promise in this case.
      then = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
      // Check that Torque load elimination works.
      static_assert(nativeContext == LoadNativeContext(context));
      goto Enqueue;
    }
  } label Enqueue {
    // 13. Let job be NewPromiseResolveThenableJob(promise, resolution,
    
    // 代码逻辑与规范一致,把 NewPromiseResolveThenableJob 送入微任务队列
    const task = NewPromiseResolveThenableJobTask(
        promise, UnsafeCast<JSReceiver>(resolution),
        UnsafeCast<Callable>(then));
    // 14. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
    // 15. Return undefined.
    
    // 插入 microtask 队列
    return EnqueueMicrotask(task.context, task);
  }
}

通过 resolutionMap.prototype == promisePrototype 判断是否为 Promise,发现 onFulfilled 执行结果是一个 Promise 的时候,会创建 NewPromiseResolveThenableJob 并插入 microtask 队列中。这里实际上就是与我们手写代码存在差异的地方,也是多出的一次微任务创建的位置。在 ECMAScript® 2022 中也有说明这一块的规范 👇

image.png

image.png

接着,我们看一下 PromiseResolveThenableJob 源码 里面到底是做了什么 👇

// https://tc39.es/ecma262/#sec-promiseresolvethenablejob
transitioning builtin
PromiseResolveThenableJob(implicit context: Context)(
    promiseToResolve: JSPromise, thenable: JSReceiver, then: JSAny): JSAny {
  const nativeContext = LoadNativeContext(context);
  const promiseThen = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
  const thenableMap = thenable.map;
  if (TaggedEqual(then, promiseThen) && IsJSPromiseMap(thenableMap) &&
      !IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() &&
      IsPromiseSpeciesLookupChainIntact(nativeContext, thenableMap)) {
      
    // PerformPromiseThen 方法也是 JS Promise then 方法的底层调用
    return PerformPromiseThen(
        UnsafeCast<JSPromise>(thenable), UndefinedConstant(),
        UndefinedConstant(), promiseToResolve);
  } else {
    const funcs =
        CreatePromiseResolvingFunctions(promiseToResolve, False, nativeContext);
    const resolve = funcs.resolve;
    const reject = funcs.reject;
    try {
      return Call(
          context, UnsafeCast<Callable>(then), thenable, resolve, reject);
    } catch (e) {
      return Call(context, UnsafeCast<Callable>(reject), Undefined, e);
    }
  }
}

PerformPromiseThen 方法实际上也是 Promise then 方法的底层核心方法,在 ECMAScript® 2022 中我们可以看到 👇

image.png

我们看一下 PromisePrototypeThen 的源码 👇

transitioning javascript builtin
PromisePrototypeThen(js-implicit context: NativeContext, receiver: JSAny)(
    onFulfilled: JSAny, onRejected: JSAny): JSAny {
  // 1. Let promise be the this value.
  // 2. If IsPromise(promise) is false, throw a TypeError exception.
  const promise = Cast<JSPromise>(receiver) otherwise ThrowTypeError(
      MessageTemplate::kIncompatibleMethodReceiver, 'Promise.prototype.then',
      receiver);

  // 3. Let C be ? SpeciesConstructor(promise, %Promise%).
  const promiseFun = UnsafeCast<JSFunction>(
      context[NativeContextSlot::PROMISE_FUNCTION_INDEX]);

  // 4. Let resultCapability be ? NewPromiseCapability(C).
  let resultPromiseOrCapability: JSPromise|PromiseCapability;
  let resultPromise: JSAny;
  label AllocateAndInit {
    const resultJSPromise = NewJSPromise(promise);
    resultPromiseOrCapability = resultJSPromise;
    resultPromise = resultJSPromise;
  }
  // onFulfilled 和 onRejected 是 then 接收的两个参数
  const onFulfilled = CastOrDefault<Callable>(onFulfilled, Undefined);
  const onRejected = CastOrDefault<Callable>(onRejected, Undefined);

  // 5. Return PerformPromiseThen(promise, onFulfilled, onRejected,
  //    resultCapability).
  // 这里是上面 ECMAScript 截图中对应的第5点,Return PerformPromiseThen
  PerformPromiseThenImpl(
      promise, onFulfilled, onRejected, resultPromiseOrCapability);
  // 返回一个新的 Promise
  return resultPromise;
}

再来看一下 PerformPromiseThen 的源码 👇

// https://tc39.es/ecma262/#sec-performpromisethen
transitioning builtin
PerformPromiseThen(implicit context: Context)(
    promise: JSPromise, onFulfilled: Callable|Undefined,
    onRejected: Callable|Undefined, resultPromise: JSPromise|Undefined): JSAny {
    
  // 调用 PerformPromiseThenImpl 方法
  PerformPromiseThenImpl(promise, onFulfilled, onRejected, resultPromise);
  return resultPromise;
}

对比一下,我们发现他们实际上都是调用了 PerformPromiseThenImpl 方法来处理核心逻辑的,我们再看一下 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 状态的分支
    // The {promise} is still in "Pending" state, so we just record a new
    // PromiseReaction holding both the onFulfilled and onRejected callbacks.
    // Once the {promise} is resolved we decide on the concrete handler to
    // push onto the microtask queue.
    const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
    // 拿到 Promise 的 reactions_or_result 字段
    const promiseReactions =
        UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
    // 考虑一个 Promise 可能会有多个 then 的情况,reaction 是个链表
    // 存储 Promise then 中传入的回调函数
    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;
    if (promise.Status() == PromiseState::kFulfilled) {
      handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
      microtask = NewPromiseFulfillReactionJobTask(
          handlerContext, reactionsOrResult, onFulfilled,
          resultPromiseOrCapability);
    } else
      deferred {
        assert(promise.Status() == PromiseState::kRejected);
        handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
        microtask = NewPromiseRejectReactionJobTask(
            handlerContext, reactionsOrResult, onRejected,
            resultPromiseOrCapability);
        if (!promise.HasHandler()) {
          runtime::PromiseRevokeReject(promise);
        }
      }
    
    // fulfilled 和 rejected 状态时,将 onRejected onFulfilled 放入微任务队列
    // 等待执行
    EnqueueMicrotask(handlerContext, microtask);
  }
  promise.SetHasHandler();
}

这里我们再次看到了熟悉的 EnqueueMicrotask(),它的出现意味着又有新的微任务被创建,这个与我们手写 Promise 实现中的处理逻辑基本一致,也就是 then 调用时所创建的那次微任务。

所以这里我们总结一下原生 Promise 创建两次微任务的位置

  • 第一次: 在发现 Promise.resolve(4) 的时候,创建 NewPromiseResolveThenableJob,并将其送入微任务队列

  • 第二次: 在处理 Promise.resolve(4) 的时候,调用 then 方法时,内部创建了微任务来处理回调函数

写在最后

特别感谢知乎@徐鹏跃 在 Promise V8 源码解析这块提供的支持,为文章提供了很多关键信息。

另外关于这道面试题,我也创建了知乎问题,得到了很多非常棒的回答,也推荐大家去看看 promise.then 中 return Promise.resolve 后,发生了什么?

长文整理不易,记得 点赞 👍 支持一下哦 😘

参考资料: