事件循环严格遵循“同步优先、微任务插队、宏任务排队、渲染穿插”规则:同步任务先执行;宏任务结束后立即清空所有微任务;宏任务按fifo顺序执行;浏览器在微任务后、下一宏任务前进行ui渲染,并动态优化高优输入响应。

事件循环的任务分配不是随机的,而是严格遵循一套优先级和时序规则。核心在于“同步优先、微任务插队、宏任务排队、渲染穿插”这四条主线。
同步任务必须先执行完
所有同步代码会直接进入调用栈,按顺序从上到下执行,中间不插入任何异步回调。哪怕 setTimeout 设为 0ms,它的回调也得等当前整个同步脚本块彻底跑完才能进队列。比如:
- console.log("a") → 立即输出
- setTimeout(() => console.log("b"), 0) → 进宏任务队列,暂不执行
- console.log("c") → 立即输出
- 结果一定是 a → c → b,不会出现 b 插在中间
微任务总在本轮宏任务末尾清空
每当一个宏任务(如 script 脚本、setTimeout 回调、click 事件处理)执行完毕,事件循环不会马上取下一个宏任务,而是先检查并一次性执行完所有待处理的微任务。
- Promise.then、queueMicrotask()、async/await 的后续逻辑都属于微任务
- 即使在微任务执行过程中又产生新的微任务(比如 then 里再写一个 then),也会被追加到当前微任务队列末尾,并在本轮全部执行完
- 它不跨轮次——微任务不会留到下一轮事件循环
宏任务按入队顺序逐个取出
宏任务队列是先进先出(FIFO)结构,但每次只取一个执行。常见宏任务包括:
- script 标签整体代码(初始宏任务)
- setTimeout / setInterval 回调
- 用户交互事件(click、input 等)
- I/O 回调(如 fetch 的 .then,注意:fetch 本身是宏任务触发,但其 .then 是微任务)
- UI 渲染(浏览器环境特有,在微任务之后、下一个宏任务之前发生)
浏览器会主动干预任务调度以保障响应性
实际运行中,浏览器不是机械地按队列顺序硬执行,而会动态调整:
- 高优先级输入事件(如鼠标移动、键盘按键)可能被提前调度,避免卡顿
- 长时间运行的同步代码会被浏览器标记为“可能阻塞”,甚至触发长任务警告
- 如果某轮宏任务执行时间过长,浏览器可能推迟渲染或合并微任务,但不会改变“微任务→渲染→宏任务”的基本节奏











