Event Loop

170 阅读3分钟

Event Loop (事件循环) 是 JavaScript 实现异步编程的核心机制,它结合浏览器的多进程架构、渲染管线等共同工作,实现了高效的非阻塞运行。

一、浏览器多进程架构基础

现代浏览器采用多进程架构,主要包含:

  1. 浏览器主进程:负责界面显示、用户交互、子进程管理等
  2. 渲染进程(Renderer):每个标签页一个,负责页面渲染、脚本执行等
    • 主线程:执行JS、DOM解析、样式计算、布局、绘制等
    • 工作线程:Web Worker、Service Worker等
    • 合成线程:图层合成
    • 光栅线程:将图层分块并光栅化
  3. GPU进程:处理GPU加速任务
  4. 网络进程:负责资源加载
  5. 插件进程:管理插件

二、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. 执行流程

  1. 从宏任务队列中取出一个任务执行
  2. 执行过程中产生的微任务进入微任务队列
  3. 当前宏任务执行完毕后,立即执行所有微任务
  4. 如有必要,执行UI渲染
  5. 检查Web Worker任务
  6. 重复上述过程
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一帧)包含:

  1. JavaScript执行:通过Event Loop调度
  2. 样式计算:Recalculate Style
  3. 布局:Layout/Reflow
  4. 绘制:Paint
  5. 合成:Composite

渲染时机

浏览器会在以下时机考虑渲染:

  • 每完成一个Event Loop循环检查是否需要渲染
  • 当requestAnimationFrame回调执行后
  • 当IntersectionObserver等API触发时
function animationLoop() {
  // 在渲染前执行,适合做动画更新
  requestAnimationFrame(() => {
    updateAnimation();
    animationLoop();
  });
}
animationLoop();

四、CSS渲染与Event Loop的交互

  1. 样式计算时机

    • DOM修改后不会立即计算样式
    • 浏览器会批量处理样式变更,通常在微任务之后
    • 强制同步布局会触发立即计算(性能杀手)
  2. 布局抖动(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';
    }
    
  3. CSS动画优化

    • 使用transform/opacity属性(跳过布局和绘制,直接合成)
    • 避免在动画中使用height/margin等触发布局的属性

五、多进程协作示例

当用户点击一个按钮时:

  1. 浏览器进程接收点击事件,转发给渲染进程
  2. 渲染进程的主线程执行事件回调(宏任务)
  3. 回调中修改DOM,产生样式/布局计算任务
  4. 微任务队列执行
  5. 检查是否需要渲染:
    • 是:执行requestAnimationFrame回调
    • 计算样式 → 布局 → 绘制 → 合成
  6. 渲染进程将绘制结果提交给GPU进程
  7. GPU进程将结果输出到屏幕

六、性能优化实践

  1. 任务拆分

    // 长任务拆分为多个小任务
    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);
      }
    }
    
  2. 优先使用微任务:当需要确保某些操作在UI更新前完成时

  3. 合理使用requestIdleCallback

    requestIdleCallback((deadline) => {
      while(deadline.timeRemaining() > 0) {
        // 执行低优先级任务
      }
    });
    
  4. 避免强制同步布局

    • 批量读取DOM属性
    • 批量写入DOM修改

七、特殊场景处理

  1. Web Worker通信

    • 通过postMessage通信,消息处理作为宏任务
    • 不阻塞主线程
  2. IntersectionObserver

    • 回调作为微任务执行
    • 高效实现懒加载
  3. MutationObserver

    • 微任务队列中批量处理DOM变更
    • 比Mutation Events更高效