JavaScript 微任务与宏任务完全指南

JavaScript 微任务与宏任务完全指南

前言

先看这道经典面试题:

javascript 复制代码
console.log("1");

setTimeout(() => {
    console.log("2");
}, 0);

Promise.resolve().then(() => {
    console.log("3");
});

console.log("4");

输出顺序是 1 → 4 → 3 → 2,而不是 1 → 2 → 3 → 4

为什么 setTimeout 写了 0 秒延迟,却排在最后?
为什么 PromisesetTimeout 先执行?

要搞懂这些,必须理解 JavaScript 的**事件循环(Event Loop)**机制。


一、先搞懂一个前提:JavaScript 是单线程的

复制代码
JavaScript 只有一个线程(一个工人)

它一次只能做一件事

想象一下:

复制代码
你是一个厨师(JS 引擎),只有一双手

你不能同时炒两个菜
你只能做完一个,再做下一个

那问题来了:如果遇到很耗时的任务(比如网络请求),难道要傻等吗?

答案是:不等!交给别人去做,做完了通知我。

这就引出了异步事件循环


二、任务的三种分类

JavaScript 中的代码分为三类:

复制代码
┌─────────────────────────────────────────────┐
│                 所有任务                      │
│                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │ 同步任务  │  │ 微任务   │  │  宏任务   │  │
│  │          │  │          │  │          │  │
│  │ 立即执行  │  │ 插队VIP  │  │ 排队普通  │  │
│  └──────────┘  └──────────┘  └──────────┘  │
└─────────────────────────────────────────────┘

1. 同步任务(立即执行)

javascript 复制代码
console.log("1");    // 同步
const a = 1 + 2;     // 同步
console.log("4");    // 同步

遇到就立刻执行,不等待。

2. 微任务(Microtask)------ VIP 队列

javascript 复制代码
Promise.resolve().then(() => { ... })    // Promise 回调
async/await                              // 本质也是 Promise
queueMicrotask(() => { ... })            // 手动添加微任务
MutationObserver                         // DOM 变化监听

3. 宏任务(Macrotask)------ 普通队列

javascript 复制代码
setTimeout(() => { ... })                // 定时器
setInterval(() => { ... })               // 循环定时器
setImmediate(() => { ... })              // Node.js 环境
I/O 操作                                 // 文件读写、网络请求
UI 渲染                                  // 浏览器页面渲染

三、用餐厅比喻理解

复制代码
把 JavaScript 想象成一个只有一个服务员的餐厅

┌─────────────────────────────────────────────────────┐
│                    🍽️ 餐厅                          │
│                                                     │
│  👨‍🍳 服务员(JS 主线程):一次只能服务一桌客人         │
│                                                     │
│  ┌─────────────┐                                    │
│  │ 当前桌(同步)│  ← 正在服务的客人,必须先搞定       │
│  └─────────────┘                                    │
│                                                     │
│  ┌─────────────┐                                    │
│  │ VIP 队列     │  ← 微任务:有 VIP 卡,优先服务     │
│  │ (微任务)    │                                    │
│  └─────────────┘                                    │
│                                                     │
│  ┌─────────────┐                                    │
│  │ 普通队列     │  ← 宏任务:普通客人,VIP 之后再服务  │
│  │ (宏任务)    │                                    │
│  └─────────────┘                                    │
└─────────────────────────────────────────────────────┘

服务顺序:

复制代码
1️⃣ 先把当前桌的客人全部服务完(同步代码)
2️⃣ 看看 VIP 队列有没有人(微任务),全部服务完
3️⃣ 再从普通队列叫一个人(宏任务)
4️⃣ 服务完这个人后,再看 VIP 队列有没有新人
5️⃣ 重复 3-4 步...

四、事件循环(Event Loop)机制

核心规则

复制代码
┌──────────────────────────────────────────┐
│              事件循环规则                   │
│                                          │
│  1. 执行所有同步代码(调用栈清空)           │
│              ↓                            │
│  2. 清空微任务队列(全部执行完)             │
│              ↓                            │
│  3. 取出一个宏任务执行                      │
│              ↓                            │
│  4. 再次清空微任务队列                      │
│              ↓                            │
│  5. 回到第 3 步,循环往复...                │
│                                          │
└──────────────────────────────────────────┘

流程图

复制代码
    ┌─────────────┐
    │  开始执行代码  │
    └──────┬──────┘
           │
           ▼
    ┌─────────────┐
    │ 执行同步代码  │ ← 遇到异步就放到对应队列
    └──────┬──────┘
           │
           ▼
    ┌─────────────────┐    有    ┌──────────────┐
    │ 微任务队列有任务? │ ──────→ │ 执行所有微任务 │
    └────────┬────────┘         └──────┬───────┘
             │ 没有                     │
             │ ←────────────────────────┘
             ▼
    ┌─────────────────┐    有    ┌──────────────┐
    │ 宏任务队列有任务? │ ──────→ │ 执行一个宏任务 │
    └────────┬────────┘         └──────┬───────┘
             │ 没有                     │
             │                 回到检查微任务 ↑
             ▼
    ┌─────────────┐
    │  程序结束     │
    └─────────────┘

五、回到开头的代码,逐行分析

javascript 复制代码
console.log("1");                    // ① 同步

setTimeout(() => {                   // ② 宏任务 → 放入宏任务队列
    console.log("2");
}, 0);

Promise.resolve().then(() => {       // ③ 微任务 → 放入微任务队列
    console.log("3");
});

console.log("4");                    // ④ 同步

第一阶段:执行所有同步代码

复制代码
代码从上到下执行:

第①行:console.log("1")  →  同步 → 立即执行 → 输出 1 ✅
第②行:setTimeout(...)   →  异步 → 放入【宏任务队列】
第③行:Promise.then(...) →  异步 → 放入【微任务队列】
第④行:console.log("4")  →  同步 → 立即执行 → 输出 4 ✅

此时输出:1, 4

┌────────────────────────────────────┐
│  调用栈(已清空)                    │
├────────────────────────────────────┤
│  微任务队列:[() => console.log("3")]│
├────────────────────────────────────┤
│  宏任务队列:[() => console.log("2")]│
└────────────────────────────────────┘

第二阶段:清空微任务队列

复制代码
微任务队列有任务 → 取出执行

执行:() => console.log("3")  → 输出 3 ✅

此时输出:1, 4, 3

┌────────────────────────────────────┐
│  调用栈(已清空)                    │
├────────────────────────────────────┤
│  微任务队列:[]  (已清空)           │
├────────────────────────────────────┤
│  宏任务队列:[() => console.log("2")]│
└────────────────────────────────────┘

第三阶段:取出一个宏任务执行

复制代码
宏任务队列有任务 → 取出一个执行

执行:() => console.log("2")  → 输出 2 ✅

此时输出:1, 4, 3, 2

┌────────────────────────────────────┐
│  调用栈(已清空)                    │
├────────────────────────────────────┤
│  微任务队列:[]                      │
├────────────────────────────────────┤
│  宏任务队列:[]                      │
└────────────────────────────────────┘

最终结果

复制代码
1 → 4 → 3 → 2

六、为什么 setTimeout(fn, 0) 不是立即执行?

很多人的疑问:我写的 0 毫秒啊,为什么不立即执行?

javascript 复制代码
setTimeout(() => {
    console.log("2");
}, 0);  // 0 毫秒

0 毫秒不代表"立即执行",而是"尽快放入宏任务队列"。

复制代码
setTimeout(fn, 0) 的意思:

❌ 不是:0 毫秒后执行 fn
✅ 而是:0 毫秒后把 fn 放入宏任务队列,等轮到它再执行

就像你去银行取号:
- 0 延迟 = 立刻拿到号
- 但你还得等前面的人(同步代码 + 微任务)办完

七、更复杂的例子

例1:微任务中产生新的微任务

javascript 复制代码
console.log('1');

setTimeout(() => {
    console.log('2');
}, 0);

Promise.resolve().then(() => {
    console.log('3');
    Promise.resolve().then(() => {
        console.log('4');
    });
});

console.log('5');
分析过程
复制代码
第一阶段:同步代码
  → 输出 1
  → setTimeout 放入宏任务队列
  → Promise.then 放入微任务队列
  → 输出 5

此时输出:1, 5

第二阶段:清空微任务队列
  → 执行:输出 3
  → 执行过程中又产生了新的微任务(输出4)→ 放入微任务队列
  → 微任务队列还没清空!继续执行
  → 执行:输出 4
  → 微任务队列清空了

此时输出:1, 5, 3, 4

第三阶段:宏任务
  → 执行:输出 2

最终输出:1, 5, 3, 4, 2

🔑 关键:微任务执行过程中产生的新微任务,也会在本轮全部执行完,不会留到下一轮!

复制代码
微任务就像 VIP 客人:

VIP 在被服务时说:"我朋友也来了,也是 VIP"
服务员:"好的,先生,马上也服务他"

而不是让他的朋友去普通队列排队

例2:宏任务和微任务交替

javascript 复制代码
console.log('1');

setTimeout(() => {
    console.log('2');
    Promise.resolve().then(() => {
        console.log('3');
    });
}, 0);

setTimeout(() => {
    console.log('4');
}, 0);

Promise.resolve().then(() => {
    console.log('5');
});

console.log('6');
分析过程
复制代码
┌─────────────────────────────────────────────────┐
│ 第一阶段:同步代码                                 │
├─────────────────────────────────────────────────┤
│ console.log('1')         → 输出 1                │
│ setTimeout(输出2+微任务)  → 宏任务队列             │
│ setTimeout(输出4)         → 宏任务队列             │
│ Promise.then(输出5)       → 微任务队列             │
│ console.log('6')         → 输出 6                │
│                                                 │
│ 输出:1, 6                                       │
│ 微任务队列:[输出5]                                │
│ 宏任务队列:[输出2+微任务, 输出4]                   │
├─────────────────────────────────────────────────┤
│ 第二阶段:清空微任务                                │
├─────────────────────────────────────────────────┤
│ 执行:输出 5                                      │
│                                                 │
│ 输出:1, 6, 5                                    │
│ 微任务队列:[]                                    │
│ 宏任务队列:[输出2+微任务, 输出4]                   │
├─────────────────────────────────────────────────┤
│ 第三阶段:取一个宏任务                              │
├─────────────────────────────────────────────────┤
│ 执行:输出 2                                      │
│ 执行过程中:Promise.then(输出3) → 放入微任务队列     │
│                                                 │
│ 输出:1, 6, 5, 2                                 │
│ 微任务队列:[输出3]                                │
│ 宏任务队列:[输出4]                                │
├─────────────────────────────────────────────────┤
│ 第四阶段:清空微任务(宏任务执行后必检查)             │
├─────────────────────────────────────────────────┤
│ 执行:输出 3                                      │
│                                                 │
│ 输出:1, 6, 5, 2, 3                              │
│ 微任务队列:[]                                    │
│ 宏任务队列:[输出4]                                │
├─────────────────────────────────────────────────┤
│ 第五阶段:取一个宏任务                              │
├─────────────────────────────────────────────────┤
│ 执行:输出 4                                      │
│                                                 │
│ 输出:1, 6, 5, 2, 3, 4                           │
└─────────────────────────────────────────────────┘

最终输出:1, 6, 5, 2, 3, 4


例3:async/await(本质是微任务)

javascript 复制代码
async function foo() {
    console.log('1');             // 同步
    const result = await bar();   // 等待 bar(),之后的代码变成微任务
    console.log('2');             // 微任务
}

async function bar() {
    console.log('3');             // 同步
}

console.log('4');
foo();
console.log('5');
await 到底干了什么?
javascript 复制代码
// 这段代码:
async function foo() {
    console.log('1');
    await bar();
    console.log('2');
}

// 等价于:
function foo() {
    console.log('1');
    bar().then(() => {
        console.log('2');   // await 后面的代码 = .then() 里的回调 = 微任务
    });
}
执行过程
复制代码
console.log('4')      → 输出 4(同步)
foo()
  → console.log('1')  → 输出 1(同步)
  → await bar()
    → console.log('3') → 输出 3(同步,bar 内部)
    → await 后面的代码放入微任务队列
console.log('5')      → 输出 5(同步)

同步结束,清空微任务:
  → console.log('2')  → 输出 2

最终输出:4, 1, 3, 5, 2

八、速查表

微任务 vs 宏任务 分类

复制代码
┌────────────────────────────────────────────┐
│                微任务(Microtask)            │
│                                            │
│  • Promise.then / catch / finally          │
│  • async/await(await 之后的代码)           │
│  • queueMicrotask()                        │
│  • MutationObserver                        │
│  • process.nextTick()  ← Node.js 专属      │
│                                            │
├────────────────────────────────────────────┤
│                宏任务(Macrotask)            │
│                                            │
│  • setTimeout / setInterval                │
│  • setImmediate()      ← Node.js 专属      │
│  • I/O 操作(网络请求、文件读写)              │
│  • UI 渲染             ← 浏览器专属          │
│  • requestAnimationFrame ← 浏览器专属       │
│  • 整段 script 代码(第一个宏任务)            │
│                                            │
└────────────────────────────────────────────┘

执行优先级

复制代码
同步代码  >  微任务  >  宏任务

   1          2          3
最高优先级              最低优先级

事件循环口诀

复制代码
同步先走完,
微任务清干净,
宏任务取一个,
微任务再清净,
如此循环往复。

九、做题模板

遇到事件循环题,按这个步骤分析:

复制代码
第一步:找出所有同步代码,按顺序执行
       → 遇到 setTimeout → 扔到宏任务队列
       → 遇到 Promise.then → 扔到微任务队列
       → 遇到 await → await 那一行是同步,之后的代码是微任务

第二步:同步代码执行完 → 清空微任务队列(全部执行)

第三步:取一个宏任务执行

第四步:执行完宏任务 → 清空微任务队列

第五步:重复第三步

十、总结

复制代码
┌─────────────────────────────────────────────┐
│              JavaScript 事件循环              │
│                                             │
│  🔴 同步代码:立即执行,最高优先级              │
│                                             │
│  🟡 微任务:同步代码执行完后立即执行             │
│     → Promise.then                          │
│     → async/await 之后的代码                  │
│     → 一次性全部清空(包括执行中新产生的)        │
│                                             │
│  🔵 宏任务:微任务清空后才执行                   │
│     → setTimeout / setInterval              │
│     → 一次只执行一个,然后检查微任务             │
│                                             │
│  执行顺序:同步 → 微任务 → 宏任务 → 微任务 → ...  │
│                                             │
└─────────────────────────────────────────────┘

🎯 一句话总结:同步代码是正餐必须先吃完,微任务是 VIP 插队优先,宏任务是普通排队慢慢来。每服务完一个普通客人(宏任务),都要先看看有没有新的 VIP(微任务)。

后记

2026年4月16日13点37分于上海,在opus 4.6辅助下完成。

相关推荐
知行合一。。。2 小时前
Python--05--面向对象(属性,方法)
android·开发语言·python
青梅橘子皮2 小时前
C语言---指针的应用以及一些面试题
c语言·开发语言·算法
浅时光_c3 小时前
3 shell脚本编程
linux·开发语言·bash
Evand J3 小时前
【三维轨迹目标定位,CKF+RTS,MATLAB程序】基于CKF与RTS平滑的三维非线性目标跟踪(距离+方位角+俯仰角)
开发语言·matlab·目标跟踪
今天又在写代码4 小时前
java-v2
java·开发语言
competes4 小时前
慈善基金投资底层逻辑应用 顶层代码低代码配置平台开发结构方式数据存储模块
java·开发语言·数据库·windows·sql
Ulyanov5 小时前
用Pyglet打造AI数字猎人:从零开始的Python游戏开发与强化学习实践
开发语言·人工智能·python
独自归家的兔5 小时前
OCPP 1.6 协议详解:StatusNotification 状态通知指令
开发语言·数据库·spring boot·物联网
希望永不加班5 小时前
Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别
java·开发语言·后端·spring·代理模式