EventLoop事件循环机制

952 阅读3分钟

事件循环(Event Loop)是 JavaScript 处理异步操作的核心机制,确保单线程的执行模型下任务高效且不阻塞地运行。以下是其核心机制和工作流程的详细解释:


1. 核心概念

(1)单线程模型

JavaScript 是单线程的,同一时间只能执行一个任务。所有同步任务按顺序执行,异步任务(如定时器、网络请求)则通过事件循环机制调度。

(2)任务队列

异步任务的回调函数被分为两类,存放在不同的队列中:

  • 宏任务队列(MacroTask Queue):包括 setTimeoutsetInterval、DOM 事件回调、I/O 操作(如文件读写)等。
  • 微任务队列(MicroTask Queue):包括 Promise.thenMutationObserverprocess.nextTick(Node.js 特有)等。

2. 事件循环的工作流程

  1. 执行同步代码
    所有同步任务直接进入执行栈(Call Stack)执行,遇到异步任务时,将其回调注册到对应的队列中。

  2. 处理微任务队列
    当执行栈清空后,事件循环会依次执行微任务队列中的所有任务,直到队列为空
    注意:微任务具有高优先级,执行期间新产生的微任务会被添加到当前队列末尾,并在此次循环中执行。

  3. 执行一个宏任务
    从宏任务队列中取出一个任务执行(如最早的 setTimeout 回调)。

  4. 重复步骤 2 和 3
    循环处理微任务队列 → 执行一个宏任务 → 处理微任务队列 → ……,形成事件循环。


3. 关键特性

  • 微任务优先于宏任务:每次执行栈清空后,先处理所有微任务,再执行一个宏任务。
  • 渲染时机:在浏览器中,页面渲染(UI更新)通常发生在微任务队列处理完毕之后、下一个宏任务执行之前。
  • 阻塞风险:长时间运行的同步代码或大量微任务会阻塞事件循环,导致页面卡顿。

4. 示例分析

console.log("Start"); // 同步任务

setTimeout(() => console.log("Timeout"), 0); // 宏任务

Promise.resolve()
  .then(() => console.log("Promise 1")) // 微任务
  .then(() => console.log("Promise 2")); // 微任务

console.log("End"); // 同步任务

输出顺序

StartEndPromise 1Promise 2Timeout

执行过程

  1. 同步任务 StartEnd 依次执行。
  2. 微任务队列中的 Promise 1Promise 2 依次执行。
  3. 最后执行宏任务队列中的 Timeout

5. 浏览器 vs Node.js 的事件循环

  • 浏览器:以宏任务和微任务为核心,与渲染流程紧密结合。
  • Node.js:使用更复杂的分阶段模型(如 timerspollcheck 等阶段),process.nextTick 优先级高于微任务。

6. 常见误区

  • setTimeout(fn, 0) 并非立即执行:它只是尽快将回调加入宏任务队列,实际执行需等待当前执行栈和微任务队列清空。
  • 微任务嵌套可能导致死循环
    function loop() {
      Promise.resolve().then(loop);
    }
    loop();
    
    这段代码会阻塞后续任务执行,因为微任务队列永远无法清空。

7. 最佳实践

  • 避免长时间同步任务:将复杂任务拆解为小块,使用 setTimeoutrequestIdleCallback 分步执行。
  • 合理使用微任务:如需要高优先级更新,但注意不要过度阻塞。
  • Web Workers:将计算密集型任务移至子线程,避免阻塞主线程。