事件循环:宏任务和微任务
概念引入
这个概念是在学习vue的时候遇到的,vue官方文档对mounted
钩子函数有一段这样的描述:
注意
mounted
不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在mounted
内部使用vm.$nextTick
但是在实际使用中可以发现,对于异步子组件,vm.$nextTick
不一定百分百有效。
这里就涉及到了宏任务和微任务的问题,在文章的最后我们可以得到答案。什么是宏任务和微任务
由于
Javascript
是单线程的脚本语言,如果所有代码都是同步执行,当遇到耗时操作时,就会使得浏览器进入假死状态。所以异步任务诞生了。
当执行同步任务遇到一个异步任务时,就在event table
(事件表)中注册回调函数,同步任务继续执行。期间异步任务完成时,回调函数会被放入event queue
(事件队列)。
异步任务之间也是有区别的。宿主环境(浏览器、node)提供的方法是宏任务,例如setTimeout, setInterval。语言标准(js引擎)提供的是微任务,例如Promise。
当同步任务执行完成,依次执行微任务队列中的所有微任务。执行完所有微任务后,从宏任务队列中获取新的宏任务执行。这样就完成了一个事件循环。
宏任务
# | 浏览器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务
# | 浏览器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
我们可以从一道经典面试题体验一下,以下代码会输出什么呢?
1 | setTimeout(_ => console.log(4)) |
在这个例子中setTimeout
就是宏任务,Promise.then
就是微任务。
进入整体代码(宏任务)后开始,按顺序执行输出1 -> 2,整体代码执行完成后,执行微任务,输出3,然后执行下一个宏任务,输出4。
回到引入问题
为什么在实际使用中,对于异步组件vm.$nextTick
操作子组件的DOM不一定百分百有效呢?nextTick
实际上是一个微任务(不支持微任务的环境将回退到宏任务):
- 对于同步组件,微任务将在组件渲染后执行,所以
nextTick
执行没有问题。 - 对于异步组件,内部也有异步任务,这个时候就要看任务队列的执行顺序,所以无法保证
nexTick
达到预期效果。
参考
[1] mounted 不会承诺所有的子组件也都一起被挂载,为什么会有这种情况发生??
[2] 微任务、宏任务与Event-Loop