在前端事件循环(Event Loop)里,“宏任务”和“微任务”是两条完全不同的队列。
一句话先记住:
一次事件循环只取一个宏任务 → 执行完该宏任务 → 把本次循环产生的所有微任务全部清空 → 浏览器视情况渲染 → 进入下一轮循环。
1. 到底谁算宏任务,谁算微任务?
分类 | 典型 API / 场景 | 入队队列 |
---|---|---|
宏任务 (macrotask) | script 整体代码、setTimeout、setInterval、setImmediate(Node)、I/O、UI rendering、postMessage、MessageChannel、requestAnimationFrame、async 函数体本身(注意不是 await 后面的部分) | 宏任务队列 |
微任务 (microtask) | Promise.then/catch/finally、process.nextTick(Node)、queueMicrotask、MutationObserver | 微任务队列 |
2. 事件循环的“一帧”长什么样(浏览器端)
- 从宏任务队列里取出最老的一个任务执行(例如一个 setTimeout 回调)。
- 把这一步产生的所有微任务按顺序全部执行完(可嵌套产生,继续清空)。
- 浏览器判断是否需要重排/重绘;执行 requestAnimationFrame 回调。
- 如果页面不需要渲染,直接进入下一轮;否则等 GPU 合成完再进入下一轮。
也就是说:“一个宏任务”对应“一整包微任务”。
3. 代码走读例题
console.log(1); // 1. 同步代码,当前宏任务
setTimeout(() => { // 2. 新宏任务
console.log(2);
Promise.resolve().then(() => console.log(3));
}, 0);
Promise.resolve() // 3. 当前宏任务里产生微任务
.then(() => console.log(4))
.then(() => console.log(5));
console.log(6); // 1. 仍在当前宏任务
输出顺序:
1 → 6 → 4 → 5 → 2 → 3
讲解:
- script 整体是第一个宏任务,先走完同步部分(1、6)。
- 同步里遇到 Promise.then,把 4、5 依次塞进微任务队列;当前宏任务末尾立刻清空微任务,所以 4、5 紧跟着打印。
- setTimeout 的回调是新的宏任务,要排到下一圈;它执行时又产生一个微任务 3,于是 3 在 2 之后立即打印。
4. 常见“坑”
async/await 只是 Promise 的语法糖
async function f() { console.log('A'); await 1; // 此处把后面所有代码包进 Promise.resolve().then(...) console.log('B'); } f(); console.log('C'); // 结果:A → C → B
await 1
让console.log('B')
进入微任务,所以 C 先跑。process.nextTick 比 Promise.then 还快(Node 环境专属)
Node 里微任务分两个优先级:nextTickQueue > PromiseQueue。
浏览器端没有 nextTick,统一当微任务即可。“零延迟”setTimeout 不一定比 Promise 快
它只是“最小 4 ms”延迟,仍然属于下一轮宏任务,而 Promise 是本轮微任务。
5. 记忆口诀
“宏一次,微清场;先同后微,再宏下一轮。”
6. 一句话总结
- 宏任务 = “一大包”工作,浏览器每帧只做一个。
- 微任务 = “插缝”工作,当前宏任务做完后一口气全清掉。
- 写代码时只要记住:Promise.then 永远比同级的 setTimeout 先跑。