JavaScript中Microtask与Macrotask的执行机制详解

码头散步
码头散步
发布时间:2026-10-16 | 4人浏览过
原创

本文深入剖析JavaScript的事件循环(Event Loop)机制,重点区分Microtask(微任务)与Macrotask(宏任务)的执行顺序。通过经典代码案例拆解Promise、async/await与setTimeout的交互逻辑,帮助开发者理清异步任务的队列管理规则,避免常见的执行顺序误区。

从一道“面试真题”看异步执行结果

在理解复杂的异步逻辑之前,我们先通过一段典型的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')

这段代码在Node.js环境中运行或在浏览器控制台执行,其输出结果完全一致。我们将结果展示如下:

如果在浏览器环境中打开该页面并查看控制台,输出效果与Node.js无异:

核心问题:为什么输出顺序是这样的?

宏观背景:Event Loop与双队列机制

JavaScript是单线程语言。为了处理异步操作,它并不像同步代码那样线性执行,而是依赖“事件循环”(Event Loop)模型。在这个模型中,异步任务被分为两大类别:Macrotask(宏任务)和Microtask(微任务)。

当主线程执行当前脚本时:

  1. 收集阶段:如果遇到Macrotask(如定时器),将其放入Macrotask队列;如果遇到Microtask(如Promise回调),将其放入Microtask队列。
    注意:此时仅将任务加入队列,并不立即执行。
  2. 清空阶段:当前同步代码执行完毕后,优先取出所有Microtask队列中的任务依次执行。
    在执行Microtask期间,若产生新的异步任务,同样遵循放入对应队列的原则。
  3. 渲染与下一步:当Microtask队列为空后,再从Macrotask队列中取出一个任务进行执行(例如执行一次setTimeout的回调),随后可能触发UI重绘,接着再次检查Microtask。

任务分类清单

  • 属于Macrotask的任务:
    setTimeout、setInterval、I/O、UI渲染、完整的script标签代码块。
  • 属于Microtask的任务:
    Promise.then/catch/finally、async/await后续逻辑、process.nextTick(Node.js特有)、MutationObserver。

第1步:分析题目代码的执行顺序

让我们回到开头的示例,逐步拆解每一行代码的去向:

  1. 执行全局作用域初始化
    代码从上往下执行。首先打印 console.log('script start');。
  2. 注册Macrotask
    遇到 setTimeout,这是一个标准的宏任务。其回调被放入Macrotask队列,等待下一轮循环。目前状态:Macrotask中有【setTimeout】。
  3. 调用async1函数(同步执行部分)
    调用 async1()。
    - 打印 async1 start。
    - 执行 await async2()。
    · 进入 async2,打印 async2 start。
    · async2内部返回一个 new Promise。Promise构造函数内的代码是同步执行的,因此立即打印 async2 promise。
    · 由于遇到了 await,console.log('async1 end') 这一行将被包装成一个Microtask,放入Microtask队列。目前状态:Microtask中有【第4行的async1 end】。
  4. 同步Promise实例化
    创建新的 new Promise,立即打印 promise1。
    执行其后的 .then()。根据规则,第一个 .then 的回调会被推入微任务队列。目前状态:Microtask中有【第4行的async1 end】,【第26行的promise2】。
  5. 执行剩余的全局同步代码
    打印 console.log('script end');。

第2步:进入Event Loop的Microtask清理阶段

此时,所有的同步代码执行完毕,开始扫描Microtask队列:

  1. 取出队列中第一项(来自async1)
    执行 console.log('async1 end');。打印 async1 end。
  2. 取出队列中第二项(来自第一层Promise链)
    执行 console.log('promise2');。打印 promise2。
    重要细节:因为 promise2 所在的回调后面还有 .then(()=>{console.log('promise3')}),所以这个第三层的回调会被推入到当前的Microtask队列末尾。目前状态:Microtask中有【第28行的promise3】。
  3. 取出最后一项Microtask
    执行 console.log('promise3');。打印 promise3。

第3步:执行下一个Macrotask

Microtask队列已清空,引擎转而执行Macrotask队列中的下一个任务:

  1. 执行setTimeout回调
    取出之前注册的宏任务,打印 console.log('setTimeout');。

至此,整个执行流程结束。最终的控制台输出顺序为:start → async1 start → async2 start → async2 promise → promise1 → script end → async1 end → promise2 → promise3 → setTimeout。

常见误区澄清

误区一:Promise.resolve的位置影响Microtask触发时机

许多人误以为 resolve() 写在不同位置会导致不同的行为。事实上,只要是在 new Promise 的同步执行范围内调用了 resolve,其触发的异步回调都会在同一轮结束后进入队列。

以下两种写法的效果完全一致:

// 写法A
new Promise((resolve, reject) => {
  console.log('1111');
  resolve(); // 同步调用resolve
  console.log('2222'); // 这行依然会同步打印
});

// 写法B
new Promise((resolve, reject) => {
  console.log('1111');
  console.log('2222');
  resolve();
});

无论怎么写,1111 和 2222 都会在微任务排队之前打印出来。

误区二:链式调用是否会一次性塞满队列

对于 Promise.then().then() 这种链式调用,引擎并不会一次性将所有后续的 then 都丢进队列。它采取的是串行生成的策略:先放入第一个 .then 的回调;等它被执行完,它内部生成的下一个 .then 才会被放入队列。这保证了执行流的可控性,但本质上它们都在同一轮Microtask中被处理。

以上就是JavaScript中Microtask与Macrotask的执行机制及鲜为人知的知识点的详细内容,更多关于JavaScript异步编程的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表