JavaScript 事件循环:一道面试题拆透宏任务与微任务的执行顺序

举报
yd_252100366 发表于 2026/10/14 09:32:08 2026/10/14
【摘要】 从调用栈与任务队列的基本模型出发,用一道经典输出顺序题完整拆解微任务、宏任务、requestAnimationFrame 的执行时机,给出手写 Promise 链与 async/await 的等价关系,并澄清浏览器与 Node 事件循环的四个差异。

## 一、先建立模型:调用栈 + 任务队列

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 更"快"是错觉**:只是排队位置不同,不代表异步操作本身更快

## 八、总结

事件循环的所有输出顺序题,套路都是"同步 → 微任务 → 宏任务"三层排队。理解这一点不需要背题,画出调用栈和两个队列,任何变体题都能自己推出来。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。