JS 中为何存在宏任务与微任务?

115 阅读2分钟

本文是由于笔者看到了一个老生常谈的面试题:“JS 中为何存在宏任务与微任务”,但是自己觉得理解的不够深入,所以专门写一篇文章来探索。

就前端场景而言,JS由两个部分组成:

  • 语言本身(ES规范)
  • 运行环境(浏览器)

在JS中区分宏任务和微任务,本质上是将 JS 引擎和宿主环境解耦。

  1. 两者区别
特性​​微任务(Microtask)​​宏任务(Macrotask)​
​执行时机​在当前宏任务执行后、下一个宏任务执行前执行在事件循环的下一轮中执行
​任务来源​语言本身或 Promise 等 API 触发宿主环境(如浏览器、Node.js)触发
​优先级​更高优先级,会阻塞渲染较低优先级,不会阻塞渲染
​常见 API​Promise.thenMutationObserverprocess.nextTick(Node.js)setTimeoutsetIntervalDOM 事件requestAnimationFrameI/O 操作
  1. 从语言环境的角度来说:
  • 微任务(Microtask)起源于ECMAScript规范,在ES2015中,ECMAScript在"Jobs"章节里定义了微任务队列(也叫JobQueue)和抽象操作Enqueue Job。

  • Promise.then、queueMicrotask、MutationObserver 等都是由ECMAScript规范直接规定的"微任务”。

  • 常见的语言环境,比如 V8 引擎,它不关注宏任务,只关注微任务的实现。一套引擎可以适配多个宿主。

  1. 从宿主角度来说
  • 宏任务(Macrotask)起源于HTML Living Standard(浏览器主线程环境规范),HTML规范把各种异步源(定时器、用户交互、I/O回调、Ul宣染等)统称为"Tasksources",它们进入的队列即"宏任务队列"。

  • 我们知道,浏览器里有很多个进程,在浏览器渲染进程里,有GUI绘制线程、JS线程、定时器线程、网络请求线程等等,这些线程,也就是 task,对应着宏任务。这也是我前面提到的,宏任务起源于 HTML,在不同的浏览器上有不同的实行方式。

  • Nodejs:虽然借鉴了浏览器的模型,Node在官方文档里也给出了自己的事事件循环分阶段(timers、poll、check等),本质上也属于宏任务的不同阶段。

  1. 为什么存在两者?
  • JS 引擎需要任务管理机制,而浏览器同样需要,因为两者凑在了一起,所以存在了顺序机制。

  • 而我们认为语言特性,强于宿主环境,所以微任务先执行,然后再执行宏任务。

  1. 两者如何协同?
  • 微任务的底层排队和执行细节由ECMAScript规范(Jobs)来管
  • 宏任务及"任务后微任务检查点"(microtask checkpoint)由宿主环境(HTML/Node)来管。
  • 在一次事件循环迭代中,先跑一个宏任务,再跑尽所有微任务,然后才可能进行渲染或下一个宏任务。