JavaScript事件循环每轮只执行一个宏任务,随后清空全部微任务。宏任务如script、setTimeout、DOM事件等驱动循环节奏;微任务如Promise回调、queueMicrotask等在宏任务后立即批量执行,期间不插入宏任务或渲染。

JavaScript 的事件循环靠宏任务和微任务协同工作,不是“先宏后微”那么简单,而是“每轮只取一个宏任务,但紧跟着清空全部微任务”。理解这点,才能准确预判异步代码的执行顺序。
宏任务是事件循环的“节拍器”
宏任务定义了事件循环的基本节奏。每次循环只执行一个宏任务,比如:
- 整个 script 脚本(初始执行)
- setTimeout / setInterval 回调
- 用户点击、滚动等 DOM 事件回调
- fetch 响应完成、I/O 完成等系统级回调
- 浏览器 UI 渲染(在微任务清空后触发)
它像一拍一拍的鼓点,推动事件循环向前走。没有宏任务,事件循环就停摆。
微任务是“插队者”,但只插在当前宏任务之后
微任务不开启新循环,只在当前宏任务执行完、下一个宏任务开始前集中执行。常见类型有:
立即学习“Java免费学习笔记(深入)”;
- Promise.then / .catch / .finally 的回调
- queueMicrotask() 显式注册的任务
- MutationObserver 的回调(监听 DOM 变化)
- Node.js 中的 process.nextTick(仅限 Node 环境)
关键规则:只要微任务队列非空,就会持续执行直到清空,期间不会穿插任何宏任务或渲染。
一次完整的事件循环步骤
不是“宏→微→宏→微”,而是严格按以下四步循环:
- 执行当前宏任务中的同步代码(压入/弹出调用栈)
- 遇到 Promise.resolve().then(...),把回调推入微任务队列
- 当前宏任务结束,立即执行所有排队的微任务(一个接一个,无中断)
- 微任务队列清空后,浏览器可能进行 UI 渲染,然后从宏任务队列取下一个任务
例如:setTimeout(() => console.log(1)) 和 Promise.resolve().then(() => console.log(2)) 同时存在时,2 总是比 1 先输出——因为 setTimeout 是下一轮宏任务,而 Promise.then 是本轮宏任务结束后的微任务。
为什么这个区别很重要
它直接影响代码可预测性和性能表现:
- 避免“微任务风暴”:连续调用 queueMicrotask 或递归 resolve Promise,会阻塞渲染和后续宏任务,导致页面卡顿
- 控制更新时机:想确保 DOM 修改后立刻响应(如获取 offsetHeight),可用微任务;想让浏览器先渲染再处理(如动画帧后清理),要用 requestAnimationFrame 或 setTimeout
- 调试异步逻辑:async/await 底层基于 Promise,所以 await 后的代码实际是微任务,不是同步语句
事件循环机制本身不复杂,但微任务的“清空式执行”容易被忽略,正是它让 JavaScript 在单线程下兼顾响应性与一致性。










