你真的明白 promise 的 then 吗?

438 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

写这篇文章的目的:

  • 在各大论坛其实有很多文章都在讲解 Promise 的面试题,但是这些文章的观点很多都是不完全正确的,甚至有些理论都不自圆其说
  • 恰好,最近我也在研究这方面的问题,本文通过 真实论据实例演示 分析这类问题
  • 本文虽然涉及 v8 引擎 层面的分析,但作者会尽可能描述地通俗易懂

在阅读这篇文章之前希望你已经对 Promise 基本使用、 包括 异步模型 有一定的了解,否则会影响一定的阅读体验

1. 回顾

如果你没有阅读过 《其实你不知道 Promise.then》 ,可以先阅读这篇文章,在这篇文章中我们提到了很多重要的概念,例如:promiseResolveThenableJobTaskReslovePromise,接下来的篇幅我们就围绕这些内容展开

2. thenable 接口

在 ECMAScript 暴露的异步结构中,实现了 then() 方法的对象就被认为是 实现了 thenable 接口

const thenable = {
  then(resolve, reject) {}
}  
// 我们认为这就是一个 thenable 接口

ECMAScript 的 Promise 类实现了 thenable 接口,也就是 《其实你不知道 Promise.then》 这篇文章中所强调的 promise.then 的执行过程中 隐式 处理返回值为 promise promise 的意义所在。

我们说 then 方法处理 promise 会在时间顺序上落后处理非 promise 两个感受不到的异步微任务

其中一个微任务是 promiseResolveThenableJobTask,另外一个微任务是 ReslovePromise,这两个微任务有着非常紧密的关系

let promiseResolveThenableJobTask = () => { 
  p1.then((value) => { ReslovePromise(p2, value) // 传递 promise }) 
}

只有执行了 promiseResolveThenableJobTask,才会产生 ReslovePromise 这个微任务

微任务 ReslovePromise 的作用是将处理并且将处理后的 promise 传递到外部

大家有没有考虑为什么这里不同步执行 ReslovePromise,而非要创建一个 promiseResolveThenableJobTask 异步微任务,而这个任务只是一个简单的执行函数的过程

不知道大家有没有见过这种写法:

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

这里我们将一个 具有 thenable 接口的对象 传递到 promise 链中,这里面的 cb 是内置的 thenable 接口的参数,我们可以感受到 cb 就是包含了 console.log 的函数 ,所以thenbable 接口也可以作为传递 promise 的参数

让我们来看到题:对比 直接 resolve()resolve(thenable)

      const p1 = Promise.resolve({
        then(resolve) {
          // 这里面的代码是异步的
          resolve(1);
        },
      })
        .then(v => console.log(v))
        .then(v => console.log(2));

      const p2 = Promise.resolve(3)
        .then(v => console.log(v))
        .then(() => console.log(4));
        
      // 3 1 4 2

resolve 具有 thenable 接口的对象会额外多执行一个微任务,在时间顺序上落后了一个执行 then 的时间,这个多余的微任务作用就是异步执行 cb 传递参数 2,所以 p1 可以等效为

      const p1 = Promise.resolve(1)
        .then(data => data)
        .then(v => console.log(v));
        .then(v => console.log(2));
     
     const p2 = Promise.resolve(3)
        .then(v => console.log(v))
        .then(() => console.log(4));

整个过程就像是两个 promise 链交替输出: -> 3 -> 1 -> 4 -> 2

再来看一道更难的题目

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

      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 4 3 5 6

依然可以将 thenable 接口认为落后一个 then

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

输出顺序:0 -> 1 -> -> 2 -> 4 -> 3 -> 5 -> 6

总结:then 方法会根据 return 值的类型开启 不同数量的微任务

  1. return 非 thenable 接口:不落后
  2. return thenable 接口的非 promise,落后 1个 then 的时间
  3. return promise,落后 2个 then 的时间

3. promiseResolveThenableJobTask

这个时候你应该或许猜出来了,为什么有上面这个结论:

  1. return promise 的时候,js引擎为了区分 thenable 接口是否是 Promise 实例,如果不是 Promise 的实例,直接向异步微任务队列中添加了一个 ReslovePromise 微任务
  2. return promise 实例的时候首先要调用 promiseResolveThenableJobTask,产生出 ReslovePromise 这个微任务

为什么处理 promise 实例的时候要多此一举呢?

官方的原文解释: image.png
来源:ECMAScript® 2020 Language Specification (ecma-international.org)

翻译如下:此作业使用提供的 thenable 及其方法来解决给定的承诺。此过程必须作为 Job 进行,以确保在完成对任何周围代码的评估后对 then 方法进行评估。

这里的 job / 作业 意思为抽象闭包,也就是多用了一层函数包裹,我能想到的地方:就像下面的代码表现的一样:

      const p = Promise.resolve(4);

      p.then = function (resolve, reject) {
        resolve(4);
      };

      const p2 = Promise.resolve().then(() => {
        return p;
      });
      p2.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);
        });
        
      // 1 2 4 3 5 6

我们使用在 p2then 里 return 了一个 promise,但是这个 promisethen 方法被我们修改了,这里对 then 方法就行一次判断 ,只会被当做 thenable 接口,而非直接调用 promise.prototype.then

如果稍微修改下代码

      p.then = function (resolve, reject) { // 因为是 thenable 产生一个微任务
        // 这里可能会产生副作用
        
        Promise.prototype.then.call(
          p,
          value => {
            resolve(value);
          },
          reject
        );
      };
      
      // 0 1 2 3 4 5 6

你会发现 promise.prototype.then 也只能产生一个微任务,如果是使用 Promise 实例直接调 promise.prototype.then,会额外向微任务队列中添加 promiseResolveThenableJobTask 这个微任务,这样做的好处可以让可能产生的副作业延后,也就是随着 promiseResolveThenableJobTask 一起被执行掉

4. 总结

Promise 实例直接调用 then 方法并且 return thenable 接口的非 promise 时,在时间的顺序上落后处理 1个 then 的时间

结语

本篇文章关于为什么 promiseResolveThenableJobTask 存在的意义来源于我的 猜测推导 ,几乎没有任何文献能够详细的描述这一块的内容,如果你觉得文章有误,或者你有新的见解,欢迎在评论区分享

往期好文推荐: JS中的巨坑 - 数组 - 掘金 (juejin.cn)