8 个示例理清宏任务微任务执行顺序
「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
事件循环相信很多人都了解过,关于宏任务和微任务的介绍和执行顺序也有非常多的介绍,我一开始看了事件循环也觉得不就是这样吗?跟着视频里面的老师做了几道题目就觉得自己已经会了,其实不然,下次遇到另一种变体或者新情况就不会了,比如 promise 中嵌套 setTimeout 顺序会怎么样?setTimeout 中嵌套 promise 会怎么样?两者循环多套几层又会怎样?如果是 setInterval 又会怎样?
话不多说,先上宏任务和微任务的执行顺序的图
下面就做几道例题理清执行顺序
promise 中嵌套 setTimeout
Promise.resolve()
.then(function () {
setTimeout(() => {
console.log("1");
}, 0);
})
.then(function () {
console.log("2");
});
// 答案
// 2,1
顺序:外层微任务 => 内层微任务 => 内层宏任务
外层微任务,指的是 Promise.resolve().then(),内层微任务 Promise.resolve().then().then(),内存宏任务就是 Promise.resolve().then() 中的 setTimeout(),下文按顺序用 A,B,C 表示
为什么会是 2,1?
当 A 执行完后,微任务队列中又添加了 C,所以会继续执行微任务队列中的 C,而不是 B,当微任务队列没有之后才会执行 B
setTimeout 中嵌套 promise
setTimeout(() => {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
});
}, 0);
// 答案
// 1,2
这题很好理解,因为 Promise 是异步的,所以是先输出 1 再输出 2,从宏/微任务的角度,执行完 setTimeout 才会把 Promise.resolve().then() 加入微任务队列,所以它不会抢先执行
注意陷阱!
setTimeout(() => {
Promise.resolve().then(() => {
console.log(2);
});
console.log(1);
}, 0);
// 答案
// 1,2
如果把 console.log(1) 放到 Promise.resolve().then() 下面还会是 1,2 吗?正如上面所说,Promise 是异步的,Promise 不会堵塞代码,所以 console.log(1) 仍然会输出
进阶套娃
Promise.resolve().then(() => {
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
});
setTimeout(() => {
console.log(3);
Promise.resolve().then(() => {
console.log(4);
});
}, 0);
console.log(5);
// 答案
// 5,1,3,4,2
顺序:外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务
这个是相当经典的例题,它的顺序包含了第一个示例中的顺序,而这个示例的顺序也是宏/微任务在事件循环中的调度顺序,这个调度和操作系统中的优先级顺序有点像,如果用操作系统讲解这条题会是怎么样?
5 最先到达,所以 cpu 先执行 5,然后 1,3 同时达到,但是 1 优先级比 3 大,所以 cpu 执行 1,执行完 1 后 2 到达,但是优先级和 3 相同,同等优先级采取先到先执行(FCFS)策略,所以执行 3,然后 3 执行完后,4 到达,此时 cpu 有 2,4 两道进程,4 的优先级比 2 要高,所以执行 2,最后执行 4
所以如果理解足够深刻,完全可以再度精简顺序,由外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务变成先微后宏
注:面试时不要说先微后宏,我说了以后面试官就在开始在笔记本上打字
我建议按照 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 中的运行机制介绍,可以说很久又有很多扩展点
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
再来一题
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
console.log(5);
// 答案
// 1,4,3,4,2
顺序:外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务
这题不再解释,都是一样的,可以拿来练练手
setInterval 的情况
setInterval 其实也是宏任务,其顺序依然是 微任务 -> 宏任务
setInterval(function () {
console.log(1);
}, 1000);
Promise.resolve().then(function () {
console.log(2);
});
console.log(3);
// 答案
// 3,2,1,...,1
既然都说到了,setInterval 就有必要吧宏任务和微任务一些叽哩旮旯 API 拉出来遛遛了
- 宏任务,requestAnimationFrame,setImmediate
- 微任务,process.nextTick,Promise.prototype.catch,Promise.prototype.finally
注意,process.nextTick 的优先级要比 Promise.prototype.then 要高
Promise.resolve().then(function () {
console.log(1);
});
process.nextTick(() => {
console.log(2);
});
// 答案
// 2,1
对于 requestAnimationFrame 涉及到了 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,已经超出了文章的核心主线,具体可以去看一下这篇文章,深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系 - ssh-晨曦时梦见兮
总结
宏任务和微任务的执行顺序本身并不难判断,但如果是两者不断嵌套不仔细看的话可能就会掉入陷阱
补充:主代码(同步代码)也属于宏任务,但是为了避免示例中宏任务和微任务的理解出现偏差,没有直接说明,而 setTimeout 也就是实例中所说的宏任务,更具体点应该是异步宏任务
补充练习
我曾经见过一条非常绕的题目,感兴趣的可以做一下,内附超详细讲解图
setTimeout(() => {
console.log(1);
}, 0);
var p1 = new Promise((res, rej) => {
res(2);
})
.then((res) => {
console.log(res);
new Promise((res, rej) => {
res(3);
})
.then((v) => {
console.log(v);
return 4;
})
.then((v) => {
console.log(v);
});
return 5;
})
.then((v) => {
console.log(v);
});
var p2 = new Promise((res, rej) => {
res(6);
})
.then((res) => {
console.log(res);
return 7;
})
.then((v) => {
console.log(v);
});
// 答案
// 2,6,3,5,7,4,1