JavaScript事件循环是单线程下协调同步代码、宏任务与微任务执行顺序的规则:call stack清空后一次性执行所有微任务,再取下一个宏任务。

JavaScript 事件循环机制不是“多线程调度器”,它本质是单线程下协调同步代码、宏任务和微任务执行顺序的规则。理解它,关键不在背流程图,而在搞清 call stack、macrotask queue 和 microtask queue 三者如何交接控制权。
call stack 清空后才检查 microtask queue
同步代码执行时,函数调用会不断压入 call stack;一旦栈顶函数返回,就弹出。只有当 call stack 完全为空时,引擎才会去读取 microtask queue(比如 Promise.then、MutationObserver 回调),并**一次性清空它**——注意:是“清空”,不是“取一个执行一个再看下一个”。
常见错误现象:
- 在
setTimeout回调里写多个Promise.resolve().then(...),误以为它们会穿插在后续宏任务之间执行 - 以为
await后的代码属于“下一个 tick”,其实它只是被编译成Promise链,仍归微任务调度
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 用
console.log+ 注释标记每个任务类型,比靠记忆更可靠 -
queueMicrotask是显式插入微任务的标准 API,比手动Promise.resolve().then更语义清晰
macrotask queue 每次只取一个,执行完再查 microtask
宏任务包括:setTimeout、setInterval、I/O、UI 渲染、script 标签加载等。事件循环每次从宏任务队列取**第一个**执行,执行完后——不管这个宏任务内部又产生了多少新宏任务——都会立即转向微任务队列,清空所有待处理微任务,之后才回到宏任务队列取下一个。
性能影响明显:
- 大量
Promise链或频繁queueMicrotask可能阻塞 UI 渲染,因为渲染本身是宏任务,得等当前微任务全部跑完才能轮到 -
setTimeout(fn, 0)并不等于“立刻执行”,它只是尽快排进宏任务队列,实际要等当前同步代码 + 所有微任务都结束
示例对比:
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);
输出是 1 → 4 → 3 → 2,因为 2 是宏任务,3 是微任务。
不同浏览器/Node.js 对 microtask 的触发时机略有差异
规范要求微任务必须在每次宏任务结束后、下次宏任务开始前执行,但具体触发点有细节差别:
- Chrome / Firefox 在每次事件循环迭代末尾清空微任务队列
- Node.js v11+ 跟齐浏览器行为;v10 及以前在每个 I/O 回调后也清空一次微任务(导致某些 Promise 行为不一致)
-
requestIdleCallback是宏任务,但它在浏览器空闲时段才执行,不受常规事件循环节奏约束
兼容性建议:
- 避免依赖“微任务一定在某个 DOM 更新前执行”的假设,尤其涉及动画帧或
getBoundingClientRect - 用
queueMicrotask替代Promise.resolve().then可减少 polyfill 差异
真正容易被忽略的是:事件循环不管理内存、不决定何时 GC、也不控制 fetch 或 WebSocket 的底层连接逻辑——它只管 JS 主线程上“谁该在什么时候拿到执行权”。把宏任务想成“一帧工作”,微任务就是这帧里的“即时响应补丁”,而 call stack 就是当前正在干的活儿。活儿干完,才轮到补丁,补丁打完,才接下一帧。











