最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【V8补充篇】从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节

    正文概述 掘金(ITEM)   2021-04-21   575

    罪魁祸首还是先挂出来 ?

    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 V8 源码分析(二)

    在大致熟悉 Promise V8 源码的之后,我们再回到之前的问题 ?

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

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

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

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

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

    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 中也有说明这一块的规范 ?

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

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

    接着,我们看一下 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 中我们可以看到 ?

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

    我们看一下 Promise then 的 源码 ?

    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 创建两次微任务的位置

    1. 发现 Promise.resolve(4) 的时候,创建 NewPromiseResolveThenableJob,并将其送入微任务队列(与手写有差异);
    2. 处理 Promise.resolve(4) 的时候,调用 then 方法时,内部创建了微任务来处理回调函数(与手写类似);

    写在最后

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

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

    参考资料:

    • zhuanlan.zhihu.com/p/264944183
    • zhuanlan.zhihu.com/p/329201628
    • tc39.es/ecma262/
    • chromium.googlesource.com/v8/v8.git/+…

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

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元