JavaScript事件循环是单线程调度规则,非轮询机制;宏任务(如script、setTimeout、I/O、UI渲染)每次执行一个,微任务(如Promise.then、queueMicrotask)在当前宏任务后立即清空。

JavaScript 的事件循环不是“轮询机制”,而是单线程下协调同步代码、异步回调与 UI 渲染的调度规则;宏任务和微任务的区别不在“谁先注册”,而在“谁先执行”——每次只取一个宏任务,但会清空全部微任务队列。
宏任务有哪些?为什么 setTimeout(0) 总是比 Promise.then 晚?
宏任务代表一次完整的、可被浏览器或 Node.js 独立调度的执行单元。它包括:script(整个脚本)、setTimeout、setInterval、I/O 回调(如 fetch 完成、fs.readFile)、UI 渲染(浏览器中)等。即使你写 setTimeout(() => {}, 0),它也必须等到当前宏任务结束、所有微任务跑完后,才进入下一个宏任务阶段。
-
setTimeout是宏任务,它的回调被推入宏任务队列,等待下一轮事件循环 -
Promise.then是微任务,只要当前宏任务执行完毕,立刻执行,不等下一个宏任务 - 哪怕在
setTimeout回调里立即Promise.resolve().then(...),这个then仍属于本轮宏任务触发的微任务,优先于下一个宏任务
微任务怎么触发?哪些 API 属于微任务?
微任务本质是“当前宏任务收尾时必须立刻处理的轻量级异步逻辑”。主流微任务来源有:
-
Promise.then/.catch/.finally的回调函数 -
queueMicrotask()—— 显式插入微任务的标准 API(2025 年已全平台支持) -
MutationObserver的回调(DOM 变更后触发) - Node.js 中的
process.nextTick()(仅限 Node,且优先级高于 Promise)
注意:queueMicrotask 和 Promise.resolve().then 行为一致,但前者语义更清晰、无 Promise 构造开销;而 process.nextTick 在 Node 中会插队到所有其他微任务之前,容易引发意料外的执行顺序。
立即学习“Java免费学习笔记(深入)”;
为什么 console.log 顺序总让人困惑?看一个典型例子
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);
输出一定是 1 → 4 → 3 → 2。原因:
-
1和4是同步代码,立即进调用栈 -
setTimeout注册宏任务,进宏任务队列(等待下一轮) -
Promise.then注册微任务,进微任务队列(本轮宏任务结束后立即执行) - 所以同步执行完 → 清空微任务(
3)→ 才取下一个宏任务(2)
实际开发中容易踩的坑
微任务不是“更快的 setTimeout”,它是同步执行链的延伸,这点常被误用:
- 在微任务里做重计算(比如遍历万级数组),会阻塞 UI 渲染和下一个宏任务,用户感觉“卡顿”,因为渲染本身是宏任务
-
await后的代码属于微任务,但很多人以为它“新开一个线程”,其实它只是把后续逻辑塞进微任务队列 - 多个
Promise.then链式调用会连续触发微任务,形成“微任务风暴”,可能压垮调用栈(虽罕见,但递归式then+throw可能触发) - 测试中依赖
setTimeout模拟“下一轮”往往不准,应改用await new Promise(r => queueMicrotask(r))或await Promise.resolve()来明确等待微任务清空
真正关键的不是记住哪些是宏/微任务,而是理解:宏任务之间天然有渲染机会,微任务之间没有。只要涉及视觉反馈、输入响应或动画帧节奏,就必须小心微任务是否意外吞掉了渲染时机。











