事件循环是JS单线程下协调同步与异步执行的核心调度机制:先执行同步代码,再清空微任务队列(Promise.then、queueMicrotask等),随后执行一个宏任务(setTimeout、事件回调等),并穿插UI渲染;微任务总在宏任务结束后立即批量执行,可抢占渲染前时机,而宏任务每次仅执行一个,保障用户交互响应。

事件循环不是“后台自动运行的黑箱”,而是开发者每天都在调度的执行节奏。理解它,不是为了背概念,而是为了写出不卡顿、不丢帧、不乱序的代码。
微任务优先级必须被主动利用
Promise.then、queueMicrotask、MutationObserver 的回调总在当前宏任务结束后立刻执行,且清空整个微任务队列——这个特性可以用来“抢占渲染前的最后时机”。
- 需要确保 DOM 更新后立即读取布局(如 getBoundingClientRect),就把读取逻辑放在 Promise.resolve().then() 中,避免被后续同步代码干扰
- 批量状态更新(比如多次 setState 或多个响应式赋值)后想统一收口处理,用 queueMicrotask 包裹,比 setTimeout(0) 更及时、更可预测
- 避免在微任务中无限递归调用 queueMicrotask,否则会饿死宏任务,导致页面完全无响应
宏任务调度要尊重用户感知
setTimeout、setInterval、事件回调、I/O 完成等属于宏任务,每次只执行一个,中间穿插渲染。这意味着:延迟控制不能只看毫秒数,更要考虑它落在哪一帧里。
- 0ms 定时器不会“立刻执行”,而是排在下一个宏任务队列尾部,必然晚于所有当前微任务和本次渲染
- 动画关键帧(requestAnimationFrame)本质是浏览器控制的宏任务,优先级高于 setTimeout,适合做视觉同步;但别在里面做重计算,否则直接掉帧
- 长任务(如复杂数组遍历、JSON 解析)应手动切片,每处理几百项就用 setTimeout 或 requestIdleCallback 让出主线程,保持滚动和输入响应
避免隐式阻塞,尤其是渲染线程
事件循环本身不卡,卡的是调用栈被长同步代码占满。JS 主线程同时承载脚本执行、样式计算、布局、绘制——任何一项超时都会让页面“冻结”。
立即学习“Java免费学习笔记(深入)”;
- 不要在 for 循环或递归中做大量 DOM 操作;改用 DocumentFragment 批量插入,或用 requestIdleCallback 分批处理
- 大型数据转换(如 map/filter 数万条记录)建议用 Web Worker 搬离主线程,主线程只负责通信和渲染
- 警惕 console.log 在开发环境引发的意外性能损耗——尤其在高频回调(如 mousemove)中,日志序列化可能比业务逻辑还慢
跨环境差异要写进上线 checklist
浏览器和 Node.js 的事件循环阶段不同:Node.js 有 timers、poll、check 等多个阶段,而浏览器只有宏/微任务+渲染。同一段代码行为可能不一致。
- process.nextTick(Node.js 特有)优先级高于 Promise.then,但在浏览器中不存在;用 queueMicrotask 替代可跨平台
- MutationObserver 在浏览器中是微任务,在 Node.js 中不可用;服务端渲染(SSR)场景下需判断运行环境再注册
- 测试异步逻辑时,别只靠 setTimeout(0) 模拟“下一轮”,要用 await Promise.resolve() 或 queueMicrotask 确保走微任务路径


















