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