JavaScript事件循环是单线程下协调同步代码、宏任务与微任务执行顺序的规则:call stack清空后一次性执行所有微任务,再取下一个宏任务。
JavaScript 事件循环机制不是“多线程调度器”,它本质是单线程下协调同步代码、宏任务和微任务执行顺序的规则。理解它,关键不在背流程图,而在搞清 call stack、macrotask queue 和 microtask queue 三者如何交接控制权。
同步代码执行时,函数调用会不断压入 call stack;一旦栈顶函数返回,就弹出。只有当 call stack 完全为空时,引擎才会去读取 microtask queue(比如 Promise.then、MutationObserver 回调),并**一次性清空它**——注意:是“清空”,不是“取一个执行一个再看下一个”。
常见错误现象:
setTimeout 回调里写多个 Promise.resolve().then(...),误以为它们会穿插在后续宏任务之间执行await 后的代码属于“下一个 tick”,其实它只是被编译成 Promise 链,仍归微任务调度实操建议:
console.log + 注释标记每个任务类型,比靠记忆更可靠queueMicrotask 是显式插入微任务的标准 API,比手动 Promise.resolve().then 更语义清晰宏任务包括: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 是微任务。
规范要求微任务必须在每次宏任务结束后、下次宏任务开始前执行,但具体触发点有细节差别:
致)requestIdleCallback 是宏任务,但它在浏览器空闲时段才执行,不受常规事件循环节奏约束兼容性建议:
getBoundingClientRect
queueMicrotask 替代 Promise.resolve().then 可减少 polyfill 差异真正容易被忽略的是:事件循环不管理内存、不决定何时 GC、也不控制 fetch 或 WebSocket 的底层连接逻辑——它只管 JS 主线程上“谁该在什么时候拿到执行权”。把宏任务想成“一帧工作”,微任务就是这帧里的“即时响应补丁”,而 call stack 就是当前正在干的活儿。活儿干完,才轮到补丁,补丁打完,才接下一帧。