对宏任务、微任务的理解

93 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情

宏任务

(macro)task(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

(macro)task主要包含常见的宏任务有:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

微任务

microtask 从名字看,我们可以把它称为微任务。每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后 再开始下一次事件循环。

在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。也就是 说,macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。

microtask的这一特性,是做队列控制的最佳选择。vue进行DOM更新内部也是调用nextTick来做异步 队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的 回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。

常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的process.nextTick.

看到了MutationObserver,vue用MutationObserver是想利用它的microtask特性,而不是想做DOM 监听。核心是microtask,用不用MutationObserver都行的。事实上,vue在2.5版本中已经删去了 MutationObserver相关的代码,因为它是HTML5新增的特性,在iOS上尚有bug。

那么最优的microtask策略就是Promise了,而令人尴尬的是,Promise是ES6新增的东西,也存在兼容 问题呀。所以vue就面临一个降级策略。

**vue的降级策略 **

上面我们讲到了,队列控制的最佳选择是microtask,而microtask的最佳选择是Promise.但如果当前环 境不支持Promise,vue就不得不降级为macrotask来做队列控制了。

macrotask有哪些可选的方案呢?前面提到了setTimeout是一种,但它不是理想的方案。因为 setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。

在vue2.5的源码中,macrotask降级的方案依次是:setImmediate、MessageChannel、setTimeout. setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬。 所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法 了。

运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

流程图如下: image.png