简要总结microtask和macrotask

papilipa

前言

我是在做前端面试题中看到了setTimeout和Promise的比较,然后第一次看到了microtask和macrotask的概念,在阅读了一些文章之后发现没有一个比较全面易懂的文章,所以我尝试做一个梳理性的总结.

这道经典的面试题引起了我的兴趣

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

JavaScript的事件循环机制

首先我们先弄清楚setTimeout和Promise的共同点,也就是我第一次的看到那道面试题的疑惑点.

JavaScript 主线程拥有一个 执行栈 以及一个 任务队列,主线程会依次执行代码,当遇到函数时,会先将函数 入栈,函数运行完毕后再将该函数 出栈,直到所有代码执行完毕。

上面的例子的执行栈执行顺序应该是这样的

console.log('script start');
console.log('script end');
Promise.resolve();

而任务队列的执行顺序应该是这样的

Promise.then(function() {
  console.log('promise1');
});
Promise.then(function() {
  console.log('promise2');
});
setTimeout(function() {
  console.log('setTimeout');
}, 0);

而主线程则会在 清空当前执行栈后,按照先入先出的顺序读取任务队列里面的任务。

众所周知setTimeout和Promise.then()都属于上述异步任务的一种,那到底为什么setTimeout和Promise.then()会有顺序之分,这就是我想分析总结的问题所在了.

macrotasks(tasks) 和 microtasks

tasks

tasks的作用是为了让浏览器能够从内部获取javascript / dom的内容并确保执行栈能够顺序进行。

tasks的调度是随处可见的,例如解析HTML,获得鼠标点击的事件回调等等,在这个例子中,我们所迷惑的setTimeout也是一个tasks.

microtasks

microtasks通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为做出反应,或者做出一些异步的任务,而不需要新建一个全新的tasks。

只要执行栈没有其他javascript在执行,在每个tasks结束时,microtasks队列就会在回调后处理。在microtasks期间排队的任何其他microtasks将被添加到这个队列的末尾并进行处理。

microtasks包括mutation observer callbacks,就像上例中的promise callbacks一样。

所以上面的例子执行顺序的实质是

  • tasks =>start end以及resolve
  • microtasks =>promise1和promise2
  • tasks =>setTimeout

具体应用

需要注意的是,在两个tasks之间,浏览器会重新渲染。这也是我们需要了解tasks和microtasks的一个非常重要的原因.

Vue 中如何使用 MutationObserver 做批量处理? - 顾轶灵的回答 - 知乎

根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。

浏览器兼容问题

在__Microsoft Edge__, Firefox 40__, __iOS Safari 以及 desktop Safari 8.0.8 中setTimeout会先于Promise

该例子来自Jake Archibald-->Tasks, microtasks, queues and schedules,其中有动画来展现tasks和microtasks的具体工作流程,十分推荐阅读

//html
<div class="outer">
  <div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

在这个例子中,不同浏览器的log是不同的,如下所示

Chrome Firefox Safari edge
click click click click
promise mutate mutate click
mutate click click mutate
click mutate mutate timeout
promise timeout promise promise
mutate promise promise timeout
timeout promise timeout promise
timeout timeout timeout \

事实上Chrome是正确的,而且由此可发现microtasks并不是在tasks的结束阶段开始执行,而是在tasks中回调结束之后(只要没有正在执行的JavaScript代码)

总结

tasks会顺序执行,浏览器会在执行间隔重新渲染

microtasks会顺序执行,执行时机为

在没有JavaScript代码执行的callback之后

在每一个tasks之后


由于我是前端初学者,对于JavaScript还很不熟悉,对事件循环的进程模型不是很了解,希望这篇文章能够帮助大家.

事件循环机制建议参考文章

阮一峰-->JavaScript 运行机制详解:再谈Event Loop

HTML Living Standard — Last Updated 9 April 2018

tasks建议参考文章

Jake Archibald-->Tasks, microtasks, queues and schedules

理解 JavaScript 中的 macrotask 和 microtask

setImmediate.js --A YuzuJS production

阅读 4.8k

咸鱼已死 咸鱼万岁

31 声望
0 粉丝
0 条评论

咸鱼已死 咸鱼万岁

31 声望
0 粉丝
宣传栏