最近写 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 就会被执行。
异步任务的来源包括 setTimeout、setInterval、event handler(如 click、mousemove 等)、promise 被解决(resolve 或 reject)等。被放入 macrotask 队列的有 setTimeout、 setInterval、 setImmediate 、event handler 等。放入 microtask 队列的则是 process.nextTick(Node.js)、 promise 相关(.then/catch/finally 及 await)、 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 之前得到执行。