干货:宏任务和微任务执行顺序详解
众所周知,JavaScript
单线程执行的,所以对于一些耗时的任务,我们可以将其丢入任务队列当中,这样一来,也就不会阻碍其他同步代码的执行。等到异步任务完成之后,再去进行相关逻辑的操作。
js
在主线程中执行的顺序:宏任务 -> 宏任务 -> 宏任务 …
在每一个宏任务中又可以产生微任务,当微任务全部执行结束后执行下一个宏任务。 【宏任务 [微任务]】 -> 【宏任务 [微任务]】-> 【宏任务 [微任务]】…
宏任务
生成方法:
- 用户交互:用户在页面上进行交互操作(例如点击、滚动、输入等),会触发浏览器产生宏任务来响应用户操作。
- 网络请求:当浏览器发起网络请求(例如通过
Ajax
、Fetch
、WebSocket
等方式)时,会产生宏任务来处理请求和响应。 - 定时器:通过
JavaScript
宿主环境提供的定时器函数(例如setTimeout
、setInterval
)可以设置一定的时间后产生宏任务执行对应的回调函数。 DOM
变化:当DOM
元素发生变化时(例如节点的添加、删除、属性的修改等),会产生宏任务来更新页面。- 跨窗口通信:在浏览器中,跨窗口通信(例如通过
postMessage
实现)会产生宏任务来处理通信消息。 JavaScript
脚本执行事件;比如页面引入的script
就是一个宏任务。
重点来看下 setTimeout
setTimeout(() => {
console.log("setTimeout block");
}, 100);
while (true) {}
console.log("end here");
以上代码会输出什么?
什么都不会输出
上边代码相当于两个宏任务:
第一个宏任务就是上边的整个脚本
第二个宏任务是 setTimeout 传入的这个函数
() => {
console.log('setTimeout block')
},
第一个宏任务执行到 while true
的时候死循环了,所以自己的 console.log('end here')
不会执行。
第二个宏任务也没有机会执行到。
因此什么都不会输出。
再来看一个:
const t1 = new Date();
setTimeout(() => {
const t3 = new Date();
console.log("setTimeout block");
console.log("t3 - t1 =", t3 - t1);
}, 100);
let t2 = new Date();
while (t2 - t1 < 200) {
t2 = new Date();
}
console.log("end here");
t1
记录开始的时间,设置一个 100
毫秒执行的定时器,定时器中输出执行当前任务的时间。
那么 console.log('t3 - t1 =', t3 - t1)
输出的是多少呢?
输出答案是 200
。
同样的,上边是两个宏任务。
整个脚本是第一个宏任务。
计时器生成了第二个宏任务。
只有第一个宏任务执行结束后才会执行第二个宏任务。
所以即使定时器时间到了也不会立刻执行,只有当第一个宏任务执行结束后才会去执行定时器的任务,此时已经过去了 200
毫秒。
微任务
生成方法:
Promise
:Promise
是一种异步编程的解决方案,它可以将异步操作封装成一个Promise
对象,通过then
方法注册回调函数,当promise
变为resolve
或者reject
会将回调函数加入微任务队列中。MutationObserver
:MutationObserver
是一种可以观察DOM
变化的API
,通过监听DOM
变化事件并注册回调函数,将回调函数加入微任务队列中。process.nextTick
:process.nextTick
是Node.js
中的一个API
,它可以将一个回调函数加入微任务队列中。
重点看 Promise
的使用,关于 Promise
怎么用这里不细说了,重点放到输出顺序上。
const r = new Promise(function (resolve, reject) {
console.log("1");
resolve();
});
r.then(() => console.log("2"));
console.log("3");
上边的输出什么:
比较基础的使用。输出 1 3 2
。
new Promise
接受一个函数,返回一个 Promise
对象。值得注意的一点是传给 Promise
的那个函数会直接执行。所以会先输出 1
。
Promise
对象拥有一个 then
方法来注册回调函数,当 promise reslove 或者 reject
后会将注册函数加到微任务队列。
上边的代码因为是直接 resolve
了,所以会将 () => console.log("2")
注册到微任务队列中。
宏任务执行完毕后开始执行微任务,所以最后输出 2
。
再看下 async
和 await
:
async function method() {
await method2();
console.log(1);
}
function method2() {
const promise = new Promise((resolve) => resolve());
return promise;
}
function main() {
method();
console.log(2);
}
上边的会输出什么呢?
先输出 2
,再输出 1
。
这里需要明确一点,async
修饰的函数,相当于给当前函数包了一层 Promise。
所以
function main() {
method();
console.log(2);
}
相当于
function main() {
new Promise((resolve,reject){ resolve(method())}
console.log(2)
}
结合前边说的传给 Promise 的那个函数会直接执行。
所以先执行 resolve(method())
,进入 method
内部: 接下来是 await
的作用:遇到 await
会先执行 await
右边的逻辑,执行完之后会暂停到这里。跳出当前函数去执行之前的代码。 所以 method()
方法中,
async function method() {
await method2();
console.log(1);
}
function method2() {
const promise = new Promise((resolve) => resolve());
return promise;
}
先执行了 method2
,当 method2
返回了 Promise
后就会暂定执行,跳回 main
函数。
function main() {
new Promise((resolve,reject){ resolve(method())}
console.log(2)
}
main
函数执行完毕后才会再回到 method
方法中。
所以先输出 2
,后输出 1
。
如果想要先输出 1 再输出 2 需要怎么改呢?
async function method() {
await method2();
console.log(1);
}
function method2() {
const promise = new Promise((resolve) => resolve());
return promise;
}
async function main() {
await method(); // 这里 await 即可
console.log(2);
}
main();
再看一个:
async function method() {
new Promise((resolve) => resolve()).then(() => console.log(1));
const n = await method2();
console.log(n);
}
function method2() {
const promise = new Promise((resolve) => resolve(2));
return promise;
}
function main() {
method();
console.log(3);
}
main();
上边的会输出什么呢?
当 main
函数执行结束后,按照之前说的应该是回到 await
那里,所以应该输出 3 2 1
吗?
其实是不对的,await
还有一个特性,它会把后边执行的代码整个注册为回调函数,相当于放到了 .then 里边,如果 Promise
直接 resolve
,相当于将后边的代码放到了微任务队列中。
所以
async function method() {
new Promise((resolve) => resolve()).then(() => console.log(1));
const n = await method2();
console.log(n);
}
等价于:
async function method() {
new Promise((resolve) => resolve()).then(() => console.log(1));
new Promise((resolve) => resolve(method2())).then((n) => console.log(n));
}
在 await
之前已经有一个 Promise
把任务加到了微任务队列中。所以正确的输出顺序是 3 1 2
。
所以回到 await 继续执行其实是表象,本质上是从微任务队列中把之前要执行的代码取了出来继续执行。
如果想输出 3 2 1
,该怎么改代码呢?
可以将 new Promise((resolve) => resolve()).then(() => console.log(1));
这句中的 reslove()
函数延迟调用,通过 setTimeout
放到下一个宏任务中执行。
async function method() {
new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.log(1));
const n = await method2();
console.log(n);
}
function method2() {
const promise = new Promise((resolve) => resolve(2));
return promise;
}
function main() {
method();
console.log(3);
}
综合
如果理解了上边的,下边的内容就简单了,首先明确几个点:
- 【宏任务 [微任务]】 -> 【宏任务 [微任务]】-> 【宏任务 [微任务]】…
当宏任务和当前宏任务产生的微任务全部执行完毕后,才会执行下一个宏任务。每遇到生成的微任务就放到微任务队列中,当前宏任 务代码全部执行后开始执行微任务队列中的任务
- 传给
new Promise
的函数会直接执行 async
包装的函数相当于包了一层Promise
,因此返回的一定是一个Promise
- 执行到
await
,先执行await
右边的东西,执行完后后会暂停在await
这里,并且把后边的内容丢到then
中(再结合第5
点)。跳到外边接着执行。外边都执行完之后开始执行微任务队列 - 当
promise
变为resolve
或者reject
的时候才会将then
中注册的回调函数加入微任务队列中 setTimeout
产生宏任务
可以多读几遍下边开始正式练习,看代码的时候函数定义直接跳过,从执行函数开始看
练习
来一道魔鬼题:
async function method() {
console.log(1);
new Promise((resolve) => resolve()).then(() => console.log(2));
new Promise((resolve) => {
setTimeout(() => {
resolve();
new Promise((resolve) => resolve()).then(() => console.log(3));
}, 0);
}).then(() => console.log(4));
await method3();
console.log(5);
const n = await method2();
console.log(n);
}
function method2() {
const promise = new Promise((resolve) => {
console.log(6);
setTimeout(() => {
console.log(7);
resolve(8);
}, 0);
});
return promise;
}
function method3() {
const promise = new Promise((resolve) => {
console.log(9);
resolve();
});
return promise;
}
function main() {
method();
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(10);
});
console.log(11);
}
main();
console.log(12);
上边的代码输出什么?
分析的时候我们需要明确什么时候产生了宏任务,什么时候产生了微任务,什么时候是直接执行的,结合上边总结 6
句话和注释可以看一下:
async function method() {
console.log(1); //[1]
new Promise((resolve) => resolve()).then(() => console.log(2)); // 第 1 个宏任务中注册微任务 1 // [5]
new Promise((resolve) => {
setTimeout(() => {
resolve();
new Promise((resolve) => resolve()).then(() => console.log(3)); // 第 2 个宏任务中注册微任务 2 // [10]
}, 0); // 注册宏任务 2
}).then(() => console.log(4)); // 第 2 个宏任务中注册微任务 1 // [9]
await method3(); // 第 1 个宏任务中注册微任务 2
console.log(5); // 第 1 个宏任务中注册微任务 2 // [6]
const n = await method2(); // 第 1 个宏任务中注册微任务 2
console.log(n); // 第 1 个宏任务中注册微任务 2 // 第 3 个宏任务中注册微任务 1 // [12]
}
function method2() {
const promise = new Promise((resolve) => {
console.log(6); // [7]
setTimeout(() => {
console.log(7); // [11]
resolve(8);
}, 0); // 注册宏任务 3
});
return promise;
}
function method3() {
const promise = new Promise((resolve) => {
console.log(9); //[2]
resolve();
});
return promise;
}
function main() {
method();
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(10); // 第 1 个宏任务中注册微任务 3 // [8]
});
console.log(11); //[3]
}
main();
console.log(12); //[4]
总结
当然上边的规则也不是黄金原则,归根到底还依赖于我们运行的环境是什么,现在 js
的运行时有 V8
、Node.js
等,它们也有各自的版本。
对于下边的代码:
const p = Promise.resolve();
(async () => {
await p;
console.log("after:await");
})();
p.then(() => console.log("tick:a")).then(() => console.log("tick:b"));
按照之前规则,先执行 await p
,因为 p
已经 resolve
了,所以会把后边的代码 console.log("after:await");
加入到微任务队列中。
接着又依次把 () => console.log("tick:a")
、() => console.log("tick:b")
加到微任务队列中。
所以输出是 after:await,tick:a, tick:b
。
在浏览器中运行符合我们的想法:
在 Node.js V16
中运行符合我们的想法:
但在 Node.js V10
中运行就些许不一样了:
至于为什么就是文章开头说的了,不管输出什么,其实就是其底层代码所决定的了。再具体的原因就需要去看 Node.js
相应的源码了。
当底层的逻辑影响到我们的业务逻辑的时候,可能就真的得去看这些源码和解决方案了。