事件循环是JavaScript单线程下维持响应性与正确性的底层调度协议,由调用栈、微任务队列、宏任务队列和渲染时机共同约束;同步代码最先执行,微任务在宏任务后立即清空,宏任务逐个执行并驱动循环节奏,渲染是浏览器独有的显式环节。

事件循环不是“等一等再执行”的简单延迟,而是 JavaScript 单线程下维持响应性与正确性的底层调度协议。它决定了代码何时运行、按什么顺序运行、以及为什么 Promise 总比 setTimeout 快——这些不是约定,而是由调用栈、微任务队列、宏任务队列和渲染时机共同约束的刚性流程。
同步代码永远最先执行
所有直接写的语句(如 console.log、变量赋值、函数调用)都在当前调用栈中立即执行,不进任何队列。这是整个事件循环的起点,也是唯一“无条件抢占”的阶段。
- 哪怕写 setTimeout(fn, 0),fn 也不会立刻执行,而是被推入宏任务队列,等待本轮同步代码彻底结束
- 函数内部的同步逻辑会逐层压栈、出栈,直到调用栈清空,才开始检查队列
- 一个 try-catch 块内的错误会阻断后续同步语句,但不影响已注册的异步回调
微任务队列在每次宏任务后强制清空
微任务(Promise.then、MutationObserver、queueMicrotask)不排队等下一轮,而是在当前宏任务结束后、渲染前,一次性全部执行完毕。这个“清空”是关键:它保证了 Promise 链的连贯性,也意味着嵌套的 then 仍属于本轮微任务。
- Promise.resolve().then(() => Promise.resolve().then(...)) 中的内层 then 仍会在同一轮微任务中执行
- MutationObserver 回调与 Promise 回调同级,但执行顺序取决于注册先后,而非类型优先
- 浏览器不会在微任务执行中途插入渲染或宏任务,这是保障状态一致性的硬性规则
宏任务驱动事件循环的节奏
宏任务(setTimeout、setInterval、I/O 回调、script 整体、用户事件)构成事件循环的主干节拍。每次只取一个宏任务执行,之后必须走完微任务 → 渲染 → 下一个宏任务的完整链条。
- 即使连续调用 10 次 setTimeout(fn, 0),它们也会依次进入宏任务队列,每次循环只处理一个
- 用户点击事件和定时器回调都是宏任务,谁先注册、谁先触发,就谁先排进队列
- Node.js 中 process.nextTick 属于微任务,但优先级高于 Promise;而 setImmediate 是宏任务,排在本轮微任务和渲染之后
渲染是浏览器独有的调度锚点
浏览器把 UI 更新作为事件循环中的显式环节:每轮宏任务执行完、微任务清空后,会检查 DOM 是否变化,决定是否触发重绘或重排。这个步骤不可跳过,也不可手动触发。
- 连续修改 10 次 DOM,浏览器通常只在本轮循环末尾渲染一次,这是性能优化,也是事件循环与渲染引擎协同的结果
- requestAnimationFrame 的回调被安排在“下一次渲染前”,它既不是微任务也不是标准宏任务,而是渲染管线的特殊钩子
- 如果微任务持续排队(比如不断 new Promise(resolve => resolve()).then(...)),渲染就会被无限推迟,造成页面冻结


















