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 秒延迟,却排在最后?
为什么 Promise 比 setTimeout 先执行?
要搞懂这些,必须理解 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辅助下完成。