Event Loop (事件循环) 是 JavaScript 实现异步编程的核心机制,它结合浏览器的多进程架构、渲染管线等共同工作,实现了高效的非阻塞运行。
一、浏览器多进程架构基础
现代浏览器采用多进程架构,主要包含:
- 浏览器主进程:负责界面显示、用户交互、子进程管理等
- 渲染进程(Renderer):每个标签页一个,负责页面渲染、脚本执行等
- 主线程:执行JS、DOM解析、样式计算、布局、绘制等
- 工作线程:Web Worker、Service Worker等
- 合成线程:图层合成
- 光栅线程:将图层分块并光栅化
- GPU进程:处理GPU加速任务
- 网络进程:负责资源加载
- 插件进程:管理插件
二、Event Loop 核心机制
1. 任务队列体系
浏览器中的事件循环管理着多个任务队列:
- 宏任务队列(Macrotask Queue):
- setTimeout/setInterval回调
- DOM事件回调
- I/O操作
- UI渲染
- postMessage
- MessageChannel
- 微任务队列(Microtask Queue):
- Promise.then/catch/finally
- MutationObserver
- queueMicrotask
- process.nextTick(Node.js)
2. 执行流程
- 从宏任务队列中取出一个任务执行
- 执行过程中产生的微任务进入微任务队列
- 当前宏任务执行完毕后,立即执行所有微任务
- 如有必要,执行UI渲染
- 检查Web Worker任务
- 重复上述过程
console.log('script start'); // 宏任务
setTimeout(() => {
console.log('setTimeout'); // 新宏任务
}, 0);
Promise.resolve().then(() => {
console.log('promise1'); // 微任务
}).then(() => {
console.log('promise2'); // 微任务
});
console.log('script end'); // 宏任务继续
// 输出顺序:
// script start
// script end
// promise1
// promise2
// setTimeout
三、与渲染管线的协作
浏览器的渲染管线(60Hz下每16.6ms一帧)包含:
- JavaScript执行:通过Event Loop调度
- 样式计算:Recalculate Style
- 布局:Layout/Reflow
- 绘制:Paint
- 合成:Composite
渲染时机
浏览器会在以下时机考虑渲染:
- 每完成一个Event Loop循环检查是否需要渲染
- 当requestAnimationFrame回调执行后
- 当IntersectionObserver等API触发时
function animationLoop() {
// 在渲染前执行,适合做动画更新
requestAnimationFrame(() => {
updateAnimation();
animationLoop();
});
}
animationLoop();
四、CSS渲染与Event Loop的交互
-
样式计算时机:
- DOM修改后不会立即计算样式
- 浏览器会批量处理样式变更,通常在微任务之后
- 强制同步布局会触发立即计算(性能杀手)
-
布局抖动(Layout Thrashing):
// 不好的写法:导致多次强制同步布局 for(let i = 0; i < 100; i++) { el.style.width = (el.offsetWidth + 1) + 'px'; } // 优化写法:先读取后写入 const width = el.offsetWidth; for(let i = 0; i < 100; i++) { el.style.width = (width + i + 1) + 'px'; } -
CSS动画优化:
- 使用transform/opacity属性(跳过布局和绘制,直接合成)
- 避免在动画中使用height/margin等触发布局的属性
五、多进程协作示例
当用户点击一个按钮时:
- 浏览器进程接收点击事件,转发给渲染进程
- 渲染进程的主线程执行事件回调(宏任务)
- 回调中修改DOM,产生样式/布局计算任务
- 微任务队列执行
- 检查是否需要渲染:
- 是:执行requestAnimationFrame回调
- 计算样式 → 布局 → 绘制 → 合成
- 渲染进程将绘制结果提交给GPU进程
- GPU进程将结果输出到屏幕
六、性能优化实践
-
任务拆分:
// 长任务拆分为多个小任务 function processChunk(start) { const chunkSize = 100; for(let i = 0; i < chunkSize; i++) { processItem(start + i); } if(start + chunkSize < total) { // 使用setTimeout或requestIdleCallback让出主线程 setTimeout(() => processChunk(start + chunkSize), 0); } } -
优先使用微任务:当需要确保某些操作在UI更新前完成时
-
合理使用requestIdleCallback:
requestIdleCallback((deadline) => { while(deadline.timeRemaining() > 0) { // 执行低优先级任务 } }); -
避免强制同步布局:
- 批量读取DOM属性
- 批量写入DOM修改
七、特殊场景处理
-
Web Worker通信:
- 通过postMessage通信,消息处理作为宏任务
- 不阻塞主线程
-
IntersectionObserver:
- 回调作为微任务执行
- 高效实现懒加载
-
MutationObserver:
- 微任务队列中批量处理DOM变更
- 比Mutation Events更高效