阅读 658

五分钟新概念之宏任务与微任务

概念引入

这个概念是在学习vue的时候遇到的,vue官方文档对mounted钩子函数有一段这样的描述:

注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick

但是在实际使用中发现,对于异步子组件,vm.$nextTick不一定百分百有效。
这里就涉及到了宏任务和微任务的问题,理解了宏任务微任务和vm.$nextTick的实现原理,我们就可以知道原因。

什么是宏任务和微任务

由于Javascript是单线程的脚本语言,如果所有代码都是同步执行,当遇到耗时操作时,就会使得浏览器进入假死状态,因此异步任务应运而生。
当执行同步任务遇到一个异步任务时,就在event table(事件表)中注册回调函数,同步任务继续执行。期间异步任务完成时,回调函数会被放入event queue(事件队列)。
异步任务之间也是有区别的。宿主环境(浏览器、node)提供的方法是宏任务,例如setTimeout, setInterval。语言标准(js引擎)提供的是微任务,例如Promise。
当同步任务执行完成,依次执行微任务队列中的所有微任务。执行完所有微任务后,从宏任务队列中获取新的宏任务执行。这样就完成了一个事件循环。

事件循环

宏任务和微任务的具体API可以参考以下[2]:

宏任务[2]

# 浏览器 Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务[2]

# 浏览器 Node
process.nextTick
MutationObserver
Promise.then catch finally

我们可以从一道经典面试题体验一下事件循环,以下代码会输出什么呢?

setTimeout(_ => console.log(4)) 
new Promise(resolve => { 
   resolve()
   console.log(1)
}).then(_ => {
   console.log(3)
})
console.log(2)
复制代码

在这个例子中整体代码和setTimeout是宏任务,Promise.then是微任务。
进入整体代码(宏任务)后开始,按顺序执行输出1 -> 2,整体代码执行完成后,执行微任务,输出3,然后执行下一个宏任务,输出4。

回到引入问题

为什么在实际使用中,对于异步组件vm.$nextTick操作子组件的DOM不一定百分百有效呢?
nextTick实际上是一个微任务(不支持微任务的环境将回退到宏任务):

  • 对于同步组件,微任务将在组件渲染后执行,所以nextTick执行没有问题。
  • 对于异步组件,内部也有异步任务,这个时候就取决于任务队列的执行顺序,所以无法保证nexTick达到预期效果。

参考

[1] mounted 不会承诺所有的子组件也都一起被挂载,为什么会有这种情况发生??
[2] 微任务、宏任务与Event-Loop