JavaScript 事件循环:一道面试题拆透宏任务与微任务的执行顺序
## 一、先建立模型:调用栈 + 任务队列
JavaScript 是单线程的,所有任务分两类排队:
- **同步代码**:直接进调用栈执行,执行完才轮到别人
- **异步任务**:先挂起,满足条件后把回调推进队列
队列又分两种,这是全部执行顺序问题的根源:
| 队列 | 成员 | 执行时机 |
| --- | --- | --- |
| 微任务队列 | Promise.then、MutationObserver、queueMicrotask、async/await 后续 | **当前同步代码执行完后、渲染前全部清空** |
| 宏任务队列 | setTimeout、setInterval、I/O、UI 事件、postMessage | 每次循环取一个,取完就去看微任务 |
一次完整循环的顺序:执行一个宏任务 → 清空微任务队列 → (必要时)渲染 → 取下一个宏任务。
## 二、一道题拆透执行顺序
```js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
```
输出是 `1 4 3 2`。拆开看:
1. `console.log('1')` 同步执行
2. setTimeout 回调进宏任务队列
3. Promise.then 回调进微任务队列
4. `console.log('4')` 同步执行——**同步代码全部跑完**
5. 清空微任务队列 → 输出 `3`
6. 进入下一轮宏任务 → 输出 `2`
记住口诀:**同步跑完 → 微任务清空 → 才轮到宏任务**。setTimeout(fn, 0) 的 0 只是"尽快",不是插队。
## 三、升级版:微任务里套微任务
```js
console.log('1');
setTimeout(() => console.log('5'), 0);
Promise.resolve()
.then(() => {
console.log('2');
return Promise.resolve('3');
})
.then((v) => console.log(v));
console.log('4');
```
输出 `1 4 2 3 5`。关键点:then 回调执行期间**新产生的微任务会追加到同一轮队列尾部**,必须全部清空才能进入宏任务。所以哪怕第一个 then 里又注册了 then,也会在输出 `5` 之前跑完。
## 四、async/await 只是语法糖
```js
async function main() {
console.log('a');
await null;
console.log('b');
}
console.log('start');
main();
console.log('end');
```
输出 `start a end b`。`await` 之后的代码等价于包在微任务里的 `.then`——同步部分一路跑到 `end`,微任务阶段才输出 `b`。async 函数本身遇到 await 之前的代码仍是同步执行,这点经常被误解。
## 五、requestAnimationFrame 的位置
`requestAnimationFrame` 回调在**重绘之前**触发,介于微任务清空与渲染之间。所以想做动画计算用它,想在渲染后测量布局用 `requestIdleCallback` 或 setTimeout。顺序上:微任务 → rAF → 渲染 → 下一个宏任务。
## 六、浏览器与 Node 的四个差异
1. **宏任务分阶段**:Node 有 timers、pending、poll、check、close 多个阶段,process.nextTick 队列比微任务还优先
2. **setTimeout 精度**:浏览器最小约 4ms,Node 的 timers 阶段是 1ms
3. **微任务清空间隙**:浏览器在宏任务之间统一清空,Node 在 nextTick 之后、进入下一阶段前清空
4. **setImmediate**:Node 独有,在 poll 阶段后的 check 阶段执行,和 setTimeout 的顺序在 I/O 回调里甚至可能互换
## 七、实践中的三条结论
1. **别在微任务里塞重计算**:微任务队列不清空就不渲染,长链 Promise 会让页面掉帧
2. **setTimeout 不能当精确时钟**:受嵌套层级(5 层以上最小 4ms)和后台标签页节流(1s)影响,动画用 rAF
3. **Promise 比 setTimeout 更"快"是错觉**:只是排队位置不同,不代表异步操作本身更快
## 八、总结
事件循环的所有输出顺序题,套路都是"同步 → 微任务 → 宏任务"三层排队。理解这一点不需要背题,画出调用栈和两个队列,任何变体题都能自己推出来。
- 点赞
- 收藏
- 关注作者
评论(0)