本文是由于笔者看到了一个老生常谈的面试题:“JS 中为何存在宏任务与微任务”,但是自己觉得理解的不够深入,所以专门写一篇文章来探索。
就前端场景而言,JS由两个部分组成:
- 语言本身(ES规范)
- 运行环境(浏览器)
在JS中区分宏任务和微任务,本质上是将 JS 引擎和宿主环境解耦。
- 两者区别
特性 | 微任务(Microtask) | 宏任务(Macrotask) |
---|---|---|
执行时机 | 在当前宏任务执行后、下一个宏任务执行前执行 | 在事件循环的下一轮中执行 |
任务来源 | 语言本身或 Promise 等 API 触发 | 宿主环境(如浏览器、Node.js)触发 |
优先级 | 更高优先级,会阻塞渲染 | 较低优先级,不会阻塞渲染 |
常见 API | Promise.then 、MutationObserver 、process.nextTick (Node.js) | setTimeout 、setInterval 、DOM 事件 、requestAnimationFrame 、I/O 操作 |
- 从语言环境的角度来说:
-
微任务(Microtask)起源于ECMAScript规范,在ES2015中,ECMAScript在"Jobs"章节里定义了微任务队列(也叫JobQueue)和抽象操作Enqueue Job。
-
Promise.then、queueMicrotask、MutationObserver 等都是由ECMAScript规范直接规定的"微任务”。
-
常见的语言环境,比如 V8 引擎,它不关注宏任务,只关注微任务的实现。一套引擎可以适配多个宿主。
- 从宿主角度来说
-
宏任务(Macrotask)起源于HTML Living Standard(浏览器主线程环境规范),HTML规范把各种异步源(定时器、用户交互、I/O回调、Ul宣染等)统称为"Tasksources",它们进入的队列即"宏任务队列"。
-
我们知道,浏览器里有很多个进程,在浏览器渲染进程里,有GUI绘制线程、JS线程、定时器线程、网络请求线程等等,这些线程,也就是 task,对应着宏任务。这也是我前面提到的,宏任务起源于 HTML,在不同的浏览器上有不同的实行方式。
-
Nodejs:虽然借鉴了浏览器的模型,Node在官方文档里也给出了自己的事事件循环分阶段(timers、poll、check等),本质上也属于宏任务的不同阶段。
- 为什么存在两者?
-
JS 引擎需要任务管理机制,而浏览器同样需要,因为两者凑在了一起,所以存在了顺序机制。
-
而我们认为语言特性,强于宿主环境,所以微任务先执行,然后再执行宏任务。
- 两者如何协同?
- 微任务的底层排队和执行细节由ECMAScript规范(Jobs)来管
- 宏任务及"任务后微任务检查点"(microtask checkpoint)由宿主环境(HTML/Node)来管。
- 在一次事件循环迭代中,先跑一个宏任务,再跑尽所有微任务,然后才可能进行渲染或下一个宏任务。