理解 JavaScript 中的 macrotask 和 microtask

前言

在实现 Promise/A+ 库的过程当中,第一次据说了 JavaScript 中的 macrotask 和 microtask 的概念。而后 Google 搜索到了如下的资料:html

阅读以后结合我本身的理解,来讲说这二者的区别。node

异步任务运行机制

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

Philip Roberts - Help,I’m stuck in an event loopgithub

在那以前,咱们先说一说 JavaScript 中的事件循环机制,上面两个连接中的文章和视频很是详细的解释了该机制。咱们只简单的解释一下,先运行下面的一段代码:web

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

这里一看,setTimeout的延时为 0 ,那么是否是程序执行到这里以后就当即执行setTimeout里面的函数呢?其实不是的,此段代码最后的输出结果为:api

"script start"
"script end"
"setTimeout"

 

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

那么遇到 WebAPI(例如:setTimeoutAJAX)这些函数时,这些函数会当即返回一个值,从而让主线程不会在此处阻塞。而真正的异步操做会由浏览器执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的 任务队列 中。浏览器

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

那么咱们来看一下上面程序的执行顺序:webapp

// 1. 开始执行
console.log('script start'); // 2. 打印字符串 "script start"
setTimeout(
    function() {                 // 5. 浏览器在 0ms 以后将该函数推入任务队列
                                 //    而到第5步时才会被主线程执行
      console.log('setTimeout'); // 6. 打印字符串 "setTimeout"
    },
    0
);                       // 3. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end');   // 4. 打印字符串 "script end"
// 5. 主线程执行栈清空,开始读取 任务队列 中的任务

 

以上就是浏览器的异步任务的执行机制,核心点为:

  • 异步任务是由浏览器执行的,不论是AJAX请求,仍是setTimeout等 API,浏览器内核会在其它线程中执行这些操做,当操做完成后,将操做结果以及事先定义的回调函数放入 JavaScript 主线程的任务队列中
  • JavaScript 主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环
  • 当主线程阻塞时,任务队列仍然是可以被推入任务的。这也就是为何当页面的 JavaScript 进程阻塞时,咱们触发的点击等事件,会在进程恢复后依次执行。

Macrotasks 和 Microtasks

基本介绍

Macrotask 和 microtask 都是属于上述的异步任务中的一种,咱们先看一下他们分别是哪些 API :

  • macrotasks: setTimeoutsetIntervalsetImmediate, I/O, UI rendering
  • microtasks: process.nextTickPromisesObject.observe(废弃), MutationObserver

    setTimeout 的 macrotask ,和 Promise 的 microtask 有什么不一样呢? 咱们经过下面的代码来展示他们的不一样点:

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');

(代码来自 Tasks, microtasks, queues and schedules,原文有执行顺序的可视化操做演示,推荐观看)

在这里,setTimeout的延时为0,而Promise.resolve()也是返回一个被resolvepromise对象,即这里的then方法中的函数也是至关于异步的当即执行任务,那么他们究竟是谁在前谁在后?

咱们看看最终的运行结果(node 7.7.3):

"script start"
"script end"
"promise1"
"promise2"
"setTimeout"

 

这里的运行结果是Promise的当即返回的异步任务会优先于setTimeout延时为0的任务执行。

缘由是任务队列分为 macrotasks 和 microtasks,而Promise中的then方法的函数会被推入 microtasks 队列,而setTimeout的任务会被推入 macrotasks 队列。在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。

注:通常状况下,macrotask queues 咱们会直接称为 task queues,只有 microtask queues 才会特别指明。

那么也就是说若是个人某个 microtask 任务又推入了一个任务进入 microtasks 队列,那么在主线程完成该任务以后,仍然会继续运行 microtasks 任务直到任务队列耗尽。

而事件循环每次只会入栈一个 macrotask ,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的全部任务后再执行 macrotask

事件循环进程模型

如今咱们根据 HTML Standard - event loop processing model来描述浏览器的事件循环的进程模型:

  1. 选择最早进入 事件循环任务队列的一个任务, 若是队列中没有任务,则直接跳到第6步的 Microtask
  2. 设置 事件循环的当前运行任务为上一步所选择的任务
  3. Run: 运行所选任务
  4. 设置 事件循环的当前运行任务为 null
  5. 将刚刚第3步运行的任务从它的任务队列中删除
  6. Microtasksperform a microtask checkpoint
  7. 更新并渲染界面
  8. 返回第1步

perform a microtask checkpoint 的执行步骤:

  1. 设置 performing a microtask checkpoint 的标记为 true
  2. Microtask queue handling: 若是事件循环的 microtask queue 是空,跳到第8步 Done
  3. 选取最早进入 microtask queue 的 microtask
  4. 设置 事件循环的当前运行任务 为上一步所选择的任务
  5. Run: 执行所选取的任务
  6. 设置 事件循环的当前运行任务 为 null
  7. 将刚刚第5步运行的 microtask 从它的 microtask queue 中删除
  8. Done: For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object (此处建议查看原网页)
  9. 清理 Index Database 的事务
  10. 使 performing a microtask checkpoint 的标记为 false

在咱们的浏览器环境的事件循环中, JavaScript 脚本也会做为一个 task 被推入 task queue,咱们在运行这个事件后,该脚本中的 microtasks,tasks 才会被推入队列。

Microtask 的应用

Vue 中如何使用 MutationObserver 作批量处理 - 顾轶灵的回答

为啥要用 microtask?根据 HTML Standard,在每一个 task 运行完之后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就能够获得最新的 UI 了。反之若是新建一个 task 来作数据更新,那么渲染就会进行两次。

根据咱们上面提到的事件循环进程模型,每一次执行 task 后,而后执行 microtasks queue,最后进行页面更新。若是咱们使用 task 来设置 DOM 更新,那么效率会更低。而 microtask 则会在页面更新以前完成数据更新,会获得更高的效率。

Microtask 的实现

immediate 库是一个跨浏览器的 microtask 实现。

这个库使用原生 JavaScript 实现了可以兼容 IE6 的 microtask ,若是对实现机制比较感兴趣的能够去阅读这个库的源码,我后面会写一篇文章来详细的介绍一下其实现。

 

原文连接:https://juejin.im/entry/58d4df3b5c497d0057eb99ff

相关文章
相关标签/搜索