EventLoop事件循环机制

100 阅读4分钟

一、事件循环的核心概念

事件循环是 JavaScript 实现异步编程的基础机制,它基于 单线程 特性,通过不断循环检查任务队列,决定何时执行异步回调。其核心逻辑可概括为:

  • 主线程执行同步任务,遇到异步操作时将回调放入队列;
  • 主线程空闲时(即栈为空),从队列中取出回调执行,如此循环。

二、任务队列:宏任务与微任务

事件循环中的任务分为两类,执行优先级不同:

1. 宏任务(Macrotask)
  • 常见场景setTimeoutsetIntervalscript(整体代码块)、I/OUI 渲染等。
  • 队列特点:浏览器中通常有多个宏任务队列(如定时器队列、I/O队列),每次事件循环仅处理一个队列中的任务。
2. 微任务(Microtask)
  • 常见场景Promise.thenMutationObserverprocess.nextTick(Node.js)等。
  • 队列特点:所有微任务在同一个队列中,优先级高于宏任务。

三、事件循环的执行流程

以浏览器环境为例,事件循环的完整流程如下:

  1. 初始阶段:执行主线程中的同步代码(即第一个宏任务)。
  2. 处理微任务:同步代码执行完毕后,立即执行微任务队列中的所有任务,直至队列为空。
  3. 渲染页面:微任务执行完毕后,浏览器可能会进行 UI 渲染(非必须步骤)。
  4. 获取下一个宏任务:从宏任务队列中取出一个任务(如定时器回调),放入主线程执行。
  5. 重复步骤2-4:不断循环,直至所有任务处理完毕。

四、经典示例:理解执行顺序

console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4');
});

console.log('5');

执行结果1 → 5 → 4 → 2 → 3
解析

  1. 同步代码 console.log('1')console.log('5') 立即执行。
  2. setTimeout 是宏任务,回调进入宏任务队列。
  3. Promise.then 是微任务,回调进入微任务队列。
  4. 主线程同步代码执行完毕后,先处理微任务队列中的 console.log('4')
  5. 微任务执行完毕,取出宏任务队列中的 setTimeout 回调,执行 console.log('2'),其内部的 Promise.then 再次进入微任务队列。
  6. 当前宏任务执行完毕,再次处理微任务队列中的 console.log('3')

五、Node.js 与浏览器事件循环的差异

Node.js 的事件循环基于 libuv 库,流程与浏览器略有不同:

阶段作用宏任务类型(Node.js)
timers处理 setTimeout/setInterval定时器回调
pending callbacks处理系统操作回调I/O 回调(如文件操作)
idle, prepare内部阶段,无需关注-
poll等待新的 I/O 事件接收新的回调、处理定时器到期任务
check处理 setImmediatesetImmediate 回调
close callbacks处理关闭事件socket.close() 回调

关键差异

  • Node.js 中 process.nextTick 的优先级高于所有微任务(包括 Promise.then)。
  • setImmediate 是独立于定时器的宏任务,在 poll 阶段之后执行。

六、实战场景:避免阻塞与优化

  1. 避免长时间阻塞

    • 大量计算任务会阻塞事件循环,导致页面卡顿,可通过 requestAnimationFrameWeb Worker 拆分任务:
      // 错误示例:阻塞主线程
      function heavyTask() {
        for (let i = 0; i < 1e8; i++) {}
      }
      
      // 优化示例:分片执行
      function optimizedTask() {
        let count = 0;
        const total = 1e8;
        
        function taskSlice() {
          // 每次处理一小部分任务
          for (let i = 0; i < 1e4 && count < total; i++) {
            count++;
          }
          
          if (count < total) {
            requestAnimationFrame(taskSlice);
          }
        }
        
        taskSlice();
      }
      
  2. 微任务与宏任务的选择

    • 需立即执行的异步操作(如 Promise 链式调用)用微任务;
    • 可延迟的操作(如用户交互反馈)用宏任务,避免阻塞渲染。

七、问题

  1. 事件循环与单线程的关系?

    • 答:JavaScript 是单线程语言,事件循环通过任务队列实现异步操作,避免主线程阻塞。
  2. 宏任务和微任务的执行顺序?

    • 答:每个宏任务执行完毕后,会立即处理所有微任务,再取下一个宏任务。
  3. 如何用事件循环解释异步代码顺序?

    • 答:结合具体示例,分析同步代码、宏任务、微任务的入队与执行顺序(如前文示例)。

总结

事件循环机制是 JavaScript 异步编程的核心,理解其原理有助于写出更高效的代码,避免性能问题。关键要记住:微任务在宏任务之间执行,且同一轮循环中会清空所有微任务。在实际开发中,合理利用宏任务与微任务的优先级,可优化用户体验与代码执行效率。