一起养成写作习惯!这是我参与「掘金日新计划 · 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线程继续接管,开始下一个宏任务(从事件队列中获取)
流程图如下: