JavaScript 中 Microtask 和 Macrotask 的执行顺序详解
本文深入解析 JavaScript 单线程模型下的异步任务调度机制,详细区分 Macrotask 与 Microtask 的执行优先级。通过具体代码案例,剖析 Promise、async/await、setTimeout 等任务的入队与执行顺序,揭示 Node.js 与浏览器环境下的细微差异,帮助开发者掌握异步编程的核心逻辑。
异步任务执行示例
首先来看一段 JavaScript 代码,分析其执行后在控制台输出的具体内容。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
return new Promise((resolve, reject) => {
resolve();
console.log('async2 promise');
})
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
});
console.log('script end')在实际面试中,能够准确回答此题的前端工程师并不多见。我们将上述代码保存为 test.js 文件,并在 Node.js 环境中运行,观察到的输出结果如下:

若将相同代码嵌入网页的 script 标签中并打开页面,查看浏览器控制台,可观察到如下输出(测试环境为 Chrome 64位 63.0.3239.84):

浏览器环境的输出结果与 Node.js 完全一致。接下来探讨产生该执行顺序的原因。
Macrotask 与 Microtask 机制解析
众所周知,JavaScript 具有单线程特性(HTML5 Web Worker 除外)以及完善的异步支持机制。在单线程运行环境下,异步任务的执行时机由两个队列共同管理:Macrotask(宏任务)和 Microtask(微任务)。这两个术语仅一个字母之差,需注意区分。当主线程执行过程中遇到属于 Macrotask 的异步任务时,将其放入 Macrotask 队列;遇到 Microtask 则放入 Microtask 队列。需注意,此时任务仅被加入队列,并未立即执行。只有当当前主线程的所有同步任务执行完毕后,引擎会优先从 Microtask 队列中依次取出任务执行。在此过程中,若遇到新的异步任务,仍遵循放入对应队列的原则。待 Microtask 队列中的所有任务执行完毕后,再从 Macrotask 队列中取出一个任务执行。
常见的 Macrotask 任务包括:
setTimeout、setInterval、script 标签整体代码、I/O 操作、UI 渲染
常见的 Microtask 任务包括:
Promise、async/await、process.nextTick、Object.observe、MutationObserver
值得注意的是,即便同属 Microtask,内部也存在优先级差异。例如在 Node.js 的实现中,process.nextTick 的执行优先级高于 Promise。具体细节可参考相关技术文章(如 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ )。不过对于 async/await 和 Promise 而言,二者之间没有优先级差异。
代码执行顺序详细分析
基于上述机制,我们来逐步分析本题代码的执行流程:
【1】执行第 15 行代码,控制台打印 'script start'。
【2】执行第 16 至 18 行代码,将回调函数作为 Macrotask 放入队列(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列为空)。
【3】执行第 20 行代码,调用 async1 函数,首先打印第 2 行的 'async1 start'。
【4】接着执行第 3 行的 async2 函数,打印第 8 行的 'async2 start'。
【5】在第 9 至 12 行遇到 Promise,首先打印第 11 行的 'async2 promise'。需注意,无论 resolve 写在 new Promise 回调函数的哪个位置,其效果等同于写在最后一句。
【6】第 3 行的 async2 返回了一个 Promise,且该调用前使用了 await 修饰,因此第 4 行的后续任务被放入 Microtask 队列(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列包含第 4 行)。
【7】执行第 22 至 25 行,打印 'promise1',并将第 26 行的任务放入 Microtask 队列。注意第 28 行尚未执行,因此该行代码暂时不产生任何效果(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列包含第 4 行和第 26 行)。
【8】执行第 30 行,打印 'script end'(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列包含第 4 行和第 26 行)。
【9】脚本主线程同步任务执行结束,引擎取出一个 Microtask 执行,即第 4 行,打印 'async1 end'(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列包含第 26 行)。
【10】继续取出下一个 Microtask,即第 26 行,打印 'promise2'。由于第 26 行后跟随 then 方法,因此将第 28 行插入 Microtask 队列(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列包含第 28 行)。
【11】再取出一个 Microtask,即第 28 行,打印 'promise3'(此时 Macrotask 队列包含第 16 行的 setTimeout,Microtask 队列为空)。
【12】Microtask 队列已空,执行下一个 Macrotask,即第 16 行的 setTimeout,打印 'setTimeout',程序结束。
注意事项与链式调用规则
需要特别说明的是,以下两种写法产生的效果完全一致(resolve 的位置不影响执行结果):
写法1:
new Promise((resolve, reject) => {
console.log('1111');
resolve();
console.log('2222');
});
写法2:
new Promise((resolve, reject) => {
console.log('1111');
console.log('2222');
resolve();
});此外,针对 Promise 的链式调用,例如 new Promise(...).then(...).then(...)...,引擎每次仅将第一个 then 的内容放入 Microtask。只有当第一个 then 执行时,才会将第二个 then 放入 Microtask,而非一次性将所有 then 全部放入队列。
以上为 JavaScript 中 Microtask 和 Macrotask 执行机制的详细解析,更多关于该主题的资料请参阅本站其他相关文章。
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
JavaScript新手常犯的编码错误与最佳实践指南
2026-11-04 22:12:38
