16-宏任务与微任务

117 阅读6分钟

宏任务与微任务

由于浏览器的应用领域越来越广泛,消息队列中这种粗时间颗粒度的任务已经不能满足需求了

所以现在又出现了一种新的技术——微任务

宏任务

在之前,我们页面中的大部分任务都是在主线程上执行的,例如像渲染事件、交互事件等

为了协调这些任务有条不紊的在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,如延迟执行队列和普通的消息队列

然后主线程采用一个for循环,不断地从这些任务队列中取出任务并执行任务,这些消息队列中的任务就叫做宏任务

消息队列中的任务是通过事件循环来执行的,这里我们看看事件循环的定义:

  • 先从多个消息队列中选出一个最老的任务,叫做oldestTask
  • 然后循环系统记录任务开始执行的时间,并把这个oldestTask设置为当前正在执行的任务
  • 当任务执行完成后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个oldestTask
  • 最后统计执行完成的时长等信息

这就是浏览器对宏任务的处理过程,但是虽然宏任务可以满足我们的大部分需求,但是如果对时间精度需求比较高的需求,宏任务就难以胜任了,接下来我们来分析一下为什么宏任务难以胜任

像页面的渲染、各种IO的完成事件、用户交互的事件等都有可能随时被添加到队列中,而且添加事件是由系统操作的,JS代码不能精确掌控任务要添加到队列中的位置,所以很难控制开始执行任务的时间

像如下JS代码:

 function timerCallback2(){
     console.log(2)
 }
 function timerCallback(){
     console.log(1)
     setTimeout(timerCallback2,0)
 }
 setTimeout(timerCallback,0)

在这段代码中,我们的目的是通过定时器来设置两个回调任务,并按先后顺序执行,中间不插入其他任务

因为如果中间插入任务,那么很可能会影响到第二个定时器的执行时间

但是,实际上,很多情况我们并不能控制,因为可能系统会插入很多系统级的任务,可以使用Performance工具来查看:

宏任务缺点.png

所以,宏任务时间粒度比较大,执行的时间间隔是不能精确控制的,对一些实时性要求比较高的任务就不太符合了,比如监听DOM变化等

微任务

之前我们介绍过异步回调,执行异步回调的方式主要有两种

  • 把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数
  • 在主函数执行结束之后,当前宏任务结束之前执行回调函数,这通常以微任务形式体现

那微任务究竟是啥?

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前

那微任务如何执行的?

当JS执行一段脚本的时候,V8引擎会创建一个全局执行上下文,在创建全局上下文的同时,V8引擎也会在内部创建一个微任务队列

这个微任务队列就是来存放微任务的,因为在当前宏任务执行过程中,有时候会产生多个微任务,所以就需要使用这个微任务队列来保存这些微任务了

但是这个微任务队列是V8引擎内部使用的,我们无法通过JS直接访问

微任务如何产生?

产生微任务的方式有两种:

  • 使用MutationObserver监控某个DOM节点,然后通过JS来修改这个节点,或者为这个节点添加、删除部分子节点,当DOM节点发生变化时就会产生DOM变化记录的微任务
  • 使用Promise,当调用Promise.resolve()Promise.reject() 的时候,也会产生微任务

微任务何时被执行?

通常情况下,在当前宏任务中的JS快执行完成时,也就在JS引擎准备退出全局执行上下文并清空调用栈时,JS会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务

如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中V8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束

这是微任务的执行流程例子:

微任务1.png

微任务2.png

通过上面的分析,可以得到几点结论:

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列
  • 微任务的执行时长会影响到当前宏任务的时长
  • 一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况,微任务都早于宏任务的执行

监听DOM变化方法演变

接下来我们来分析一下微任务如何应用在MutationObserver中,MutationObserver是用来监听DOM变化的一套方法,因为有时候我们需要监视DOM变化并计时做出响应

在早期,我们需要使用轮询检测,比如使用setTimeout定时器定期检测DOM是否有改变,但是如果时间间隔太长,就会导致DOM响应不够及时,如果过短,就会导致浪费很多的无用工作量去检查DOM,让页面变得低效

2000年引入**Mutation Event解决实时性问题**,一旦DOM发生变化,就立即调用JS接口,但是这种实时性也造成了严重的性能问题,每次DOM变动渲染引擎都会去调用JS,这样会产生较大的性能开销

为了解决**Mutation Event由于同步调用JS造成的性能问题**,所以从DOM4开始,推荐使用MutationObserver代替Mutation Event

MutationObserver将响应函数改成异步调用,可以不用再每次DOM变化时都触发异步调用,而是等多次DOM变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的DOM变化,这样就不会对性能造成太大的影响

那么我们现在通过异步调用和减少触发次数来缓解了性能问题,那么应该怎么保持消息通知的及时性?我们使用的是微任务在每次DOM节点变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加到当前的微任务队列中

综上所述,MutationObserver使用的是异步+微任务的策略,通过异步操作解决了同步操作的性能问题,通过微任务解决了实时性的问题