JS中的微任务与宏任务

345 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

事件循环

事件循环是为了解决 javascript 单线程阻塞问题而做的解决方案。Event Loop 包含两类:一类是基于 Browsing Context,即浏览器环境;一类是基于Web Worker;两者运行是独立的,也就是说,每一个 javascript 运行的线程环境都有一个独立的 EventLoop,每一个 Web Worker 也有一个独立的 EventLoop。

本文的事件循环是基于 Browsing Context 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥

任务队列

根据规范,事件循环是通过任务队列的机制进行协调的。一个事件循环中可以有一个或者多个任务队列,一个任务队列便是一系列有序任务的集合,每一个任务都有一个任务源,源自同一个任务员的任务必须放到同一个任务队列,不同源来的则被添加到不同队列。setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。

在事件循环中,每一次循环操作称为 tick,每次 tick 执行的关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务,如果有则执行(一次),执行其同步代码直至结束;
  • 检查是否存在微任务(Microtasks),如果存在,则不停的执行,直到微任务全部执行完毕;
  • 更新 render(即页面重新渲染);
  • 开始下一轮 tick,主线程重复执行上述操作;

注意: ES6 规范中,microtask 称为 jobs,macrotask 称为 task 宏任务是由宿主发起的,而微任务是由 javascript 自身发起的。

宏任务(macro task)

每次执行栈(即主线程)执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件毁掉并放入执行栈中执行)

浏览器为了能够使得 js 内部宏任务与 DOM 任务有序执行,会在一个宏任务执行结束之后,另一个宏任务执行开始之前,对页面重新渲染。

宏任务包括:

  • 每个script标签加载的js资源均视为一个宏任务
  • setTimeout
  • setInterval
  • I / O
  • UI 交互事件
  • postMessage
  • MessageChannel
  • setImmediate(Node.js 环境)

微任务(micro task)

在每一个宏任务执行完之后,就会将在它执行期间产生的所有微任务都执行完毕(在渲染之前)

微任务包括:

  • Promise.then
  • Object.observe
  • MutationObserver
  • process.nextTick(Node.js 环境)

运行机制

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

  • 执行栈选择最先进入队列的宏任务(一般都是一个个 script资源),执行其同步代码直至结束;
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中;
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染;
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)。

实例

console.log("script start");
​
async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2 end");
}
async1();
​
setTimeout(function() {
  console.log("setTimeout");
​
  new Promise((resolve) => {
    console.log("s-promise1");
    resolve();
  }).then(function() {
    console.log("s-promise2");
  });
​
  setTimeout(function() {
    console.log("setTimeout1");
  }, 0);
}, 0);
​
setTimeout(function() {
  console.log("setTimeout2");
​
  setTimeout(function() {
    console.log("setTimeout3");
  }, 0);
​
  console.log("setTimeout2 end");
}, 0);
​
new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  });
​
console.log("script end");

输出顺序: 第一轮宏任务(同步):

  1. script start
  2. async2 end
  3. Promise
  4. script end

第一轮微任务(异步):

  1. async1 end
  2. promise1
  3. promise2

第二轮宏任务(setTimeout(12 行)):

  1. setTimeout
  2. s-promise1

第二轮微任务:

  1. s-promise2

第三轮宏任务(setTimeout(27 行))

  1. setTimeout2
  2. setTimeout2 end

第四轮宏任务(setTimeout(22 行))

  1. setTimeout1

第五轮宏任务(setTimeout(30 行))

  1. setTimeout3

总结

从上例我们可以看出,一个宏任务中可以有三种任务,即同步任务微任务宏任务。在执行一个宏任务时,先按顺序完成同步任务,然后按顺序完成微任务,遇到宏任务就按顺序挂起。当前的宏任务执行完毕之后,就从所有被挂起的宏任务中取出最先被挂起的宏任务,按照上面的执行方法继续执行这个宏任务。

注意:当宏任务中嵌套宏任务时,外层宏任务只需要把内层的宏任务挂起就可以按顺序执行下一个宏任务了,不需要等待内层宏任务执行完毕。宏任务的执行是按照被挂起的先后顺序来确定的,而不是按照嵌套关系来确定的。