JavaScript事件循环每次执行一个宏任务后立即清空当轮微任务队列;宏任务包括setTimeout、I/O、UI渲染等,微任务包括Promise.then、queueMicrotask、MutationObserver等;执行顺序为同步代码→微任务→下一轮宏任务。

JavaScript 的事件循环不是“先执行宏任务再执行微任务”这么简单,而是每次从宏任务队列取一个任务执行完后,**立即清空当前轮次的微任务队列**——这个“清空”动作才是理解执行顺序的关键。
宏任务和微任务分别有哪些
宏任务(macrotask)是事件循环调度的基本单位,每次只取一个执行;微任务(microtask)则在每个宏任务结束后、渲染前集中执行,且必须全部执行完才能进入下一个宏任务。
- 常见宏任务:
setTimeout、setInterval、setImmediate(Node.js)、I/O 回调、UI 渲染(浏览器)、new Promise(...).then(...)外层同步代码不算微任务,但它的then回调是微任务 - 常见微任务:
Promise.then/catch/finally、queueMicrotask、MutationObserver回调、process.nextTick(Node.js,优先级高于Promise)
执行顺序:为什么 setTimeout(setTimeout(...)) 不等于嵌套延迟
宏任务之间严格按插入顺序排队,但微任务会插在两个宏任务之间“挤占”执行时机。比如连续两次 setTimeout,哪怕第二个在第一个回调里注册,它也得等第一个宏任务执行完 + 所有微任务跑完 + 下一轮事件循环开始,才能执行。
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);
输出是 1 → 4 → 3 → 2。注意:第 3 行的 Promise.then 是微任务,它被推入微任务队列;第 2 行的 setTimeout 是宏任务,排在下一轮事件循环开头。
立即学习“Java免费学习笔记(深入)”;
微任务嵌套不会导致无限循环阻塞 UI
即使在 Promise.then 里反复调用 queueMicrotask 或返回新 Promise,它们都会被加入当前轮次的微任务队列末尾,但事件循环仍会在该轮次结束后退出微任务执行,去检查是否需要渲染或处理下一个宏任务——所以不会饿死主线程。
- 但要注意:如果微任务中触发了大量计算或同步死循环,依然会卡住页面,因为微任务是同步执行的
-
process.nextTick在 Node.js 中比Promise微任务更早执行,甚至能“插队”到当前操作结束但还没离开 JS 栈时 - 浏览器中没有
process.nextTick,不要试图 polyfill 它来模拟更高优先级
实际调试时怎么确认任务类型
不能只看函数名,要看它**注册时所处的上下文以及宿主环境实现**。例如:
-
fetch().then(...)的回调是微任务(因为基于Promise) -
fetch()本身发起网络请求是宏任务级别的 I/O 操作,但它的回调机制交由 Promise 链接管 -
requestAnimationFrame是宏任务,但它在渲染前执行,优先级介于微任务和普通setTimeout之间 - 某些库(如 Vue 的
$nextTick)内部会优先使用Promise.then,降级才用setTimeout,所以行为随环境变化
最可靠的方式是写个最小测试用例,用 console.log 打点,配合浏览器 DevTools 的 “Event Loop” 面板或 Node.js 的 --trace-event 观察实际调度顺序。










