Promise 你真的用明白了么?

文章首发自笔者的 Github前端

Promise 关于 API 这块你们应该都能熟练使用,可是和微任务相关的你可能还存在知识盲区。git

前置知识

在开始正文前,咱们先把本文涉及到的一些内容提早定个基调。github

Promise 哪些 API 涉及了微任务?

Promise 中只有涉及到状态变动后才须要被执行的回调才算是微任务,好比说 thencatchfinally ,其余全部的代码执行都是宏任务(同步执行)。数组

上图中蓝色为同步执行,黄色为异步执行(丢到微任务队列中)。promise

这些微任务什么时候被加入微任务队列?

这个问题咱们根据 ecma 规范来看:微信

  • 若是此时 Promise 状态为 pending,那么成功或失败的回调会分别被加入至 [[PromiseFulfillReactions]][[PromiseRejectReactions]] 中。若是你看过手写 Promise 的代码的话,应该能发现有两个数组存储这些回调函数。
  • 若是此时 Promise 状态为非 pending 时,回调会成为 Promise Jobs,也就是微任务。

了解完以上知识后,正片开始。异步

同一个 then,不一样的微任务执行

初级

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve().then(() => {
      console.log("then1-1");
    });
  })
  .then(() => {
    console.log("then2");
  });

以上代码你们应该都能得出正确的答案:then1 → then1-1 → then2函数

虽然 then 是同步执行,而且状态也已经变动。但这并不表明每次遇到 then 时咱们都须要把它的回调丢入微任务队列中,而是等待 then 的回调执行完毕后再根据状况执行对应操做。优化

基于此,咱们能够得出第一个结论:链式调用中,只有前一个 then 的回调执行完毕后,跟着的 then 中的回调才会被加入至微任务队列。lua

中级

你们都知道了 Promise resolve 后,跟着的 then 中的回调会立刻进入微任务队列。

那么如下代码你认为的输出会是什么?

let p = Promise.resolve();

p.then(() => {
  console.log("then1");
  Promise.resolve().then(() => {
    console.log("then1-1");
  });
}).then(() => {
  console.log("then1-2");
});

p.then(() => {
  console.log("then2");
});

按照一开始的认知咱们不可贵出 then2 会在 then1-1 后输出,可是实际状况倒是相反的。

基于此咱们得出第二个结论:每一个链式调用的开端会首先依次进入微任务队列。

接下来咱们换个写法:

let p = Promise.resolve().then(() => {
  console.log("then1");
  Promise.resolve().then(() => {
    console.log("then1-1");
  });
}).then(() => {
  console.log("then2");
});

p.then(() => {
  console.log("then3");
});

上述代码其实有个陷阱,then 每次都会返回一个新的 Promise,此时的 p 已经不是 Promise.resolve() 生成的,而是最后一个 then 生成的,所以 then3 应该是在 then2 后打印出来的。

顺便咱们也能够把以前得出的结论优化为:同一个 Promise 的每一个链式调用的开端会首先依次进入微任务队列。

高级

如下你们能够猜猜 then1-2 会在什么时候打印出来?

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve()
      .then(() => {
        console.log("then1-1");
        return 1;
      })
      .then(() => {
        console.log("then1-2");
      });
  })
  .then(() => {
    console.log("then2");
  })
  .then(() => {
    console.log("then3");
  })
  .then(() => {
    console.log("then4");
  });

这题确定是简单的,记住第一个结论就能得出答案,如下是解析:

  • 第一次 resolve 后第一个 then 的回调进入微任务队列并执行,打印 then1
  • 第二次 resolve 后内部第一个 then 的回调进入微任务队列,此时外部第一个 then 的回调所有执行完毕,须要将外部的第二个 then 回调也插入微任务队列。
  • 执行微任务,打印 then1-1then2,而后分别再将以后 then 中的回调插入微任务队列
  • 执行微任务,打印 then1-2then3 ,以后的内容就不一一说明了

接下来咱们把 return 1 修改一下,结果可就大不相同啦:

Promise.resolve()
  .then(() => {
    console.log("then1");
    Promise.resolve()
      .then(() => {
        console.log("then1-1");
        return Promise.resolve();
      })
      .then(() => {
        console.log("then1-2");
      });
  })
  .then(() => {
    console.log("then2");
  })
  .then(() => {
    console.log("then3");
  })
  .then(() => {
    console.log("then4");
  });

当咱们 return Promise.resolve() 时,你猜猜 then1-2 会什么时候打印了?

答案是最后一个才被打印出来。

为何在 then 中分别 return 不一样的东西,微任务的执行顺序竟有如此大的变化?如下是笔者的解析。

PS:then 返回一个新的 Promise,而且会用这个 Promise 去 resolve 返回值,这个概念须要你们先了解一下。

根据 Promise A+ 规范

根据规范 2.3.2,若是 resolve 了一个 Promise,须要为其加上一个 thenresolve

if (x instanceof MyPromise) {
  if (x.currentState === PENDING) {
  } else {
    x.then(resolve, reject);
  }
  return;
}

上述代码节选自手写 Promise 实现。

那么根据 A+ 规范来讲,若是咱们在 then 中返回了 Promise.resolve 的话会多入队一次微任务,可是这个结论仍是与实际不符的,所以咱们还须要寻找其余权威的文档。

根据 ECMA - 262 规范

根据规范 25.6.1.3.2,当 Promise resolve 了一个 Promise 时,会产生一个NewPromiseResolveThenableJob,这是属于 Promise Jobs 中的一种,也就是微任务。

This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.

而且该 Jobs 还会调用一次 then 函数来 resolve Promise,这也就又生成了一次微任务。

这就是为何会触发两次微任务的来源。

最后

文章到这里就完结了,你们有什么疑问均可以在评论区提出。

推荐关注个人微信公众号【前端真好玩】,工做日推送高质量文章。

image.png

笔者就任于酷家乐,家装设计行业独角兽。一流的可视化、前端技术团队,有兴趣的能够简历投递至 zx597813039@gmail.com