Promise 深入理解

135 阅读3分钟

最近写 Promise 的时候,有一个疑问:

let resolveP1;
const p1 = new Promise(function(resolve, reject) {
  resolveP1 = resolve;
});

p1.then(() => console.log(1));
p1.then(() => console.log(2));
resolveP1();

当 p1 被 resolve 的时候,是先打印出1还是先打印出2?还是随机的顺序?经过一番查找,我发现答案是“12”。根据 Promises/A+ 规范第 2.2.6.1,当一个 promise 被满足(fulfilled)时,相应的 callback 应按照当初调用 .then() 的顺序来一一执行。问题来了,如果换做是 await 呢?

let resolveP1;
const p1 = new Promise(function(resolve, reject) {
  resolveP1 = resolve;
});
const foo = async function () {
  await p1;
  console.log(2);
}

p1.then(() => console.log(1));
foo();
resolveP1();

把 console.log(2) 放进了一个异步函数 foo 里,执行,结果仍然是“12”。因为 await 在“底层”其实利用的也是 promise。上述代码大致上相当于:

let resolveP1;
const p1 = new Promise(function(resolve, reject) {
  resolveP1 = resolve;
});
const foo = function () {
  return p1.then(function() {
    console.log(2);
  });
}

p1.then(() => console.log(1));
foo();
resolveP1();

Microtask 与 Macrotask

在查找过程当中,又看到了另一段代码:

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

你可以试着写出它的结果是什么。执行以后,输出为:

script start
script end
promise1
promise2
setTimeout

首先,前两行很容易理解,放在最外部的两个 console.log 都是同步代码,会即刻执行,而 setTimeout.then 都属于异步代码。

继续看后面三行,“promise1”,“promise2”都输出在“setTimeout”的前面。看起来似乎遇到 setTimeout.then() 同时要执行时,.then() 异步的优先级似乎“更高一点”。

原因如下。JS 引擎用两个队列来管理异步任务,分别是 macrotask queue 和 microtask queue。线程不断从队列中取出任务,执行,执行完继续取出下一个,如此循环。例如,setTimeout(fn, 1000),那么在 1 秒钟后,会有一个任务 fn 被放入队列。如果此时队列为空,线程空闲,那么 fn 就会被执行。

异步任务的来源包括 setTimeoutsetInterval、event handler(如 click、mousemove 等)、promise 被解决(resolve 或 reject)等。被放入 macrotask 队列的有 setTimeoutsetIntervalsetImmediate 、event handler 等。放入 microtask 队列的则是 process.nextTick(Node.js)、 promise 相关(.then/catch/finallyawait)、 MutationObserver 等。

在每次取出下一个 macrotask 之前,线程都会先去执行所有的 microtask,直到该队列为空以后,才会取出并执行下一个 macrotask。我们的代码块(如 <script> 标签)属于 macrotask。JS 引擎会把代码块用一个函数包裹起来,放入 macrotask 队列。

因此回到原先的题目,在执行 console.log('script end'); 以后,当前的 macrotask 执行完毕了。在继续取出下一个 macrotask 之前,先清空 microtask 队列。microtask 队列里,是第一个 .then,即 console.log('promise1')。执行之后,第一个 .then 返回的 promise 随即被 resolve,于是第二个 .then 被放入了 microtask 队列。继续取出,执行,打印出“promise2”。microtask 队列不再有任务。此时取出 macrotask 队列里的任务,即 setTimeout。可以看出,在一个 microtask 里,也可以添加新的 microtask。新的 microtask 也会在 macrotask 之前得到执行。

参考资源