微任务和宏任务
微任务 和 宏任务 表示异步任务的两种分类。
微任务(microtask)和宏任务(macrotask)的关系
先给大家模拟一个场景:
这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“小四的号码为77号,前边还有22人,”之类的字样。
因为柜台工作人员同时只能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。
所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。
而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。
所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。
也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币?
无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
微任务的代表
- promise
- nodejs中的process.nextTick
宏任务的代表
- setTImeout
- setInterVal
经典面试题 - 1
setTimeout( () => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout
就是作为宏任务来存在的,而Promise.then
则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise
在实例化的过程中所执行的代码都是同步进行的,而then
中注册的回调才是异步执行的。
在同步代码执行完成后才会去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论1、2、3、4
。
经典面试题 - 2
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
本来setTimeout
已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise
的处理(临时添加业务)。
即便我们继续在Promise
中实例化Promise
,其输出依然会早于setTimeout
的宏任务:
所以输出结果1、2、3、before timeout、also before timeout、4
。
经典面试题 - 3
setTimeout(() => {
//执行后 回调一个宏事件
console.log('3')
}, 0)
console.log('1');
new Promise((resolve) => {
console.log('2');
resolve()
}).then(() => {
console.log('1');
}).then(()=>{
console.log('2')
})
不分析了,直接出结果:1、2、1、2、3
经典面试题 - 4
//主线程直接执行
console.log('1');
//丢到宏事件队列中
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
//微事件1
process.nextTick(function() {
console.log('6');
})
//主线程直接执行
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
//微事件2
console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
• 首先浏览器执行js进入第一个宏任务进入主线程, 直接打印console.log('1')
• 遇到 **setTimeout ** 分发到宏任务Event Queue中
• 遇到 process.nextTick 丢到微任务Event Queue中
• 遇到 Promise, new Promise 直接执行 输出 console.log('7');
• 执行then 被分发到微任务Event Queue中
•第一轮宏任务执行结束,开始执行微任务 打印 6,8
•第一轮微任务执行完毕,执行第二轮宏事件,执行setTimeout
•先执行主线程宏任务,在执行微任务,打印'2,4,3,5'
•在执行第二个setTimeout,同理打印 ‘9,11,10,12’
•整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
以上是在浏览器环境下执行的数据,只作为宏任务和微任务的分析,我在node环境下测试打印出来的顺序为:1,7,6,8,2,4,9,11,3,10,5,12。node环境执行结果和浏览器执行结果不一致的原因是:浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。libuv库流程大体分为6个阶段:timers,I/O callbacks,idle、prepare,poll,check,close callbacks,和浏览器的microtask,macrotask那一套有区别。
总结:
微任务和宏任务的关系
当然了,实际情况下很少会有简单的这么调用
Promise
的,一般都会在里边有其他的异步操作,比如axios
、fs.readFile
之类的操作。而这些其实就相当于注册了一个宏任务,而非是微任务。
P.S. 在Promise/A+的规范中,Promise
的实现可以是微任务,也可以是宏任务,但是普遍的共识表示(至少Chrome
是这么做的),Promise
应该是属于微任务阵营的,这是目前业界比较流行的说法:
宏任务
# | 浏览器 | Node |
---|---|---|
I/O |
✅ | ✅ |
setTimeout |
✅ | ✅ |
setInterval |
✅ | ✅ |
setImmediate |
❌ | ✅ |
requestAnimationFrame |
✅ | ❌ |
有些地方会列出来UI Rendering
,说这个也是宏任务,可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤
requestAnimationFrame
姑且也算是宏任务吧,requestAnimationFrame
在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行
微任务
# | 浏览器 | Node |
---|---|---|
process.nextTick |
❌ | ✅ |
MutationObserver |
✅ | ❌ |
Promise.then catch finally |
✅ | ✅ |
所以,明白哪些操作是宏任务、哪些是微任务就变得很关键