其实你不知道 Promise.then

1,352 阅读6分钟

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

前言

你真的完全知道 Promise 的执行顺序吗,如果你只通过 Promise A+ 规范了解 promise ,你一定不能完全理解 Promise,不信的话你来看看下面这道题

      Promise.resolve()
        .then(() => {
          console.log(0);
          return Promise.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 3 4 5 6

如果你不能清晰的说出这两种情况的过程,这篇文章一定对你有帮助。

在阅读本文之前,希望你了解:

  • Promise 基本语法
  • 事件循环模型
  • 微任务

Promise.prototype.then

then 方法是一个 异步的 微任务,解析到这行代码的时候,会把 then 之后的 Promise 链条放到 微任务队列

⭐️then 的返回值

Promise.resolve().then(() => {}); // fulfilled

then 可以链式调用的原因是因为 then 方法可以返回一个 Promise 实例,注意是 新的实例,与 Promise 链条传递的 之前的 Promise 实例 并无多大关系,而且这个新的实例 状态 一定不是 pending,知道这些内容就足够了

⭐️控制 then 返回值

这里的内容才是重点
then 处理返回值有两种情况:

  • return 值(等同于 return undefined) / 显示地 returnpromise 的值,
  • 显示 return 具有 resolvepromise 的值

如果是 returnresolve 的值(排除 reject 情况),Promise 链不会传递下去,所以这里不讨论这种情况

then 处理这两种情况决然不同,这里的观点你基本不会在任何书上看到,全部来自于 v8 引擎 内部实现

  1. 处理 returnpromise 的值
Promise.resolve().then(() => {});

image.png

这里相当于 return undefined,在被 抛出 返回值的时候,会被 Promise.resolve 包装,所以最后的 promise 状态为 fulfilled,这个包装的过程不发生在 return 的时候,而发生在整体代码执行完 抛出 返回值的过程 被包装
也就是说你 不能理解 成这样:

Promise.resolve().then(() => { return Promise.resolve(undefined) })

  1. 处理 return 具有 resolvepromise 的值
Promise1.resolve()
  .then(() => Promise2.resolve());

这里的过程很复杂,分为三步:

  • 执行 ResolvePromise (来源于 v8 引擎内部),调用 promiseResolveThenableJobTask (来源于 v8 引擎内部) 产生一个微任务放置到微任务队列,这个微任务就是一个函数
let promiseResolveThenableJobTask = () => {
    p1.then((value) => { 
        ReslovePromise(p2, value) // 传递 promise
    })
}
  • 在主线程任务清空的情况下,取出并执行 promiseResolveThenableJobTask 微任务,执行完这个任务也就是调用这个函数,会产生一个 Promise2.fulfilled 的微任务,这个微任务作用是将内部的 Promise2 的结果传递给 then 外部
  • 主线程从微任务队列取出执行 Promise2.fulfilled 任务,调用 ReslovePromise 函数 ,然后用 Promise.resolve 包装返回结果

所以,按照执行的顺序,在时间顺序上完全可以理解为插入了两个 你感受不到的微任务

也就是说,在时间顺序上落后处理 returnpromise 两个 then 的时间,那么代码可以转换(并不能化等号)为

Promise1.resolve()
  .then(() => {
    return undefined;
  })
  .then((data) => data)
  .then(data => data);

解题

再来看开头的那道题

没有添加 return Promise.resolve()

      Promise.resolve() // p1 
        .then(() => {   // p2
          console.log(0);
        })
        .then(() => {   // p3
          console.log(4);
        });

      Promise.resolve() // p4
        .then(() => {   // p5 
          console.log(1); 
        })
        .then(() => {   // p6
          console.log(2);
        })
        .then(() => {   // p7
          console.log(3);
        })
        .then(() => {   // p8
          console.log(5);
        })
        .then(() => {   // p9
          console.log(6);
        });
        
        // 0 1 4 2 3 5 6

为了简化过程,在代码注释的内容代替变量

执行顺序:
(1)执行完所有同步代码, p1 和 p2 的状态变为 fulfilled,并且先微任务队列中插入两个处理后续的 then 的微任务 [p1.fulfilled, p4.fulfilled]

(2)开始执行异步任务,从微任务队列中取出 p1.fulfilled 并执行,输出 0,微任务队列变成 [p4.fulfilled,p2.fulfiled]

(3)从微任务队列中取出 p4.fulfilled 并执行,输出 1,微任务队列变成 [p2.fulfilled,p6.fulfiled]

(4)从微任务队列中取出 p2.fulfilled 并执行,输出 4,执行完变成 p3.fulfilled,因为后面没有异步任务了,微任务队列变成 [p6.fulfiled]

(5)从微任务队列中取出 p6.fulfilled 并执行,输出 2,微任务队列变成 [p7.fulfiled]

(6) 。。。

所以你能看到两个 promise 链条 交替执行,输出的结果也就是交替的


添加 return Promise.resolve()

      Promise.resolve() // p1 
        .then(() => {   // p2
          console.log(0);
          return Promise.resolve(); // p0
        })
        .then(() => {   // p3
          console.log(4);
        });

      Promise.resolve() // p4
        .then(() => {   // p5 
          console.log(1); 
        })
        .then(() => {   // p6
          console.log(2);
        })
        .then(() => {   // p7
          console.log(3);
        })
        .then(() => {   // p8
          console.log(5);
        })
        .then(() => {   // p9
          console.log(6);
        });
        
        // 0 1 2 3 4 5 6

为了简化过程,在代码注释的内容代替变量

执行顺序:
(1)执行完所有同步代码, p1 和 p2 的状态变为 fulfilled,并且先微任务队列中插入两个处理后续的 then 的微任务 [p1.fulfilled, p4.fulfilled]

(2)开始执行异步任务,从微任务队列中取出 p1.fulfilled 并执行,输出 0,看到返回值是个 Promise,会调用 NewPromiseResolveThenableJobTask 产生一个微任务 ,微任务队列变成  [p4.fulfilled,NewPromiseResolveThenableJobTask(p1.fulfilled)]

(3)从微任务队列中取出 p4.fulfilled 并执行,输出 1,微任务队列变成  [NewPromiseResolveThenableJobTask(p1.fulfilled),p5.fulfiled]

(4)从微任务队列中取出 NewPromiseResolveThenableJobTask(p1.fulfilled) 并执行,然后向微任务队列添加 p0.fulfilled 这个微任务(作用:将 p0 传递给 p2),微任务队列变为 [p6.fulfiled, p0.fulfilled]

(5)从微任务队列中取出 p5.fulfilled 并执行,输出 2 ,微任务队列变成 [p0.fulfilled, p6.fulfiled,]

(6)从微任务队列中取出 p0.fulfilled 并执行,将 p0 传递给 p3,微任务队列变成 [p6.fulfilled, p2.fulfiled,]

(7)从微任务队列中取出 p6.fulfilled 并执行,输出 3 ,微任务队列变成 [p2.fulfilled, p7.fulfiled,]

(8)从微任务队列中取出 p2.fulfilled 并执行,输出 4,执行完变成 p3.fulfilled,因为后面没有异步任务了,微任务队列变成 [p7.fulfiled]

(7)。。。后面的过程就不用多说了吧

如果你不想了解的那么细致,你直接将代码按照时间顺序将 return promise 替换为两个 then,因为这些时间执行了两个处理 promise微任务

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

按照 returnpromise 的情况来执行,输出的代码为 0 -> 1 -> 空 -> 2 -> 空 -> 3 -> 4 -> 5 -> 6,依然是两个 promise 链条交替执行的结果

更复杂的情况

如果我们将 promise 链条后面的 then 拿进来呢?

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

      Promise.resolve()
        .then(() => {
          console.log(1);
        })
        .then(() => {
          console.log(2);
        })
        .then(() => {
          console.log(3);
        })
        .then(() => {
          console.log(5);
        })
        .then(() => {
          console.log(6);
        });
        
        // 0 1 4 2 7 3 8 5 6

执行顺序: 会在 promiseResolveThenableJobTask 处理 promise.fulfilled 之前,会将内部的 promise 链条放到微任务队列,执行完内部的 promise 链条,才会执行 promiseResolveThenableJobTask 后面的过程
所以,你也会看到交替执行的结果

Promise.resolve

上面的内容我们说 Promise.resolve 会包装 then 抛出 的结果,很多人认为 Promise.resolvenew Promise((resolve) => resolve())语法糖,这种说法是完全错误的

      const p = new Promise(() => {});
      
      p === Promise.resolve(p); // true
      p === new Promise(resolve => resolve(p)) // false

上面的代码可以很好的说明,两者最重要的区别,Promise.resolve 是幂等的,而且不会改变 promise 的状态

image.png

所以才能用 Promise.resolve 包装 thenreturn 结果并且 抛出

总结

promise.then 显示指定类型为 promise 作为返回值,会引起 js 解析引擎 额外的处理过程,在时间顺序上完全等同于插入 两个 then 的结果,你可以这样理解让这个过程变得更加简单

相信你看完本篇文章一定会加深对 promise 的理解,如果本篇文章对你有帮助或者你有不同的看法,欢迎在评论区留下你的足迹。

往期好文

JS中的巨坑 - 数组 - 掘金 (juejin.cn)