js 事件循环

17 阅读2分钟

JavaScript 的事件循环(Event Loop)是其处理异步任务的核心机制。由于 JavaScript 是单线程的,事件循环通过任务队列和循环调度实现了非阻塞的异步行为。以下是事件循环的核心概念和工作流程:

核心概念

  1. 调用栈(Call Stack)

    • 用于跟踪当前执行的代码(函数调用),后进先出(LIFO)。
    • 同步任务会直接压入栈中执行,遇到异步任务时,会将其交给浏览器 API 处理。
  2. 任务队列(Task Queue)

    • 异步任务完成后,其回调函数会被推入队列中。

    • 分为两种队列:

      • 宏任务队列(MacroTask Queue) :如 setTimeoutsetInterval、DOM 事件回调、I/O 操作等。
      • 微任务队列(MicroTask Queue) :如 Promise.thenMutationObserverqueueMicrotask 等。
  3. 事件循环的工作流程

    • 每次循环(一次 Tick)会依次处理宏任务、微任务,直到所有队列清空。

事件循环的步骤

  1. 执行一个宏任务(从宏任务队列中取出最早的任务,如 script 标签整体代码)。

  2. 执行所有微任务

    • 清空微任务队列中的所有任务(包括执行过程中新产生的微任务)。
  3. 渲染页面(如果需要):

    • 执行 UI 渲染、布局、绘制等操作(浏览器可能会跳过此步骤以优化性能)。
  4. 开始下一轮循环,处理下一个宏任务。

执行顺序规则

  1. 微任务优先级高于宏任务

    • 每次宏任务执行完毕后,会立即清空微任务队列。
  2. 同类型任务的顺序

    • 宏任务按队列顺序执行,微任务按队列顺序执行。
console.log("Start"); // 同步任务,直接执行

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

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

console.log("End"); // 同步任务,直接执行

输出顺序

StartEndPromise 1Promise 2setTimeout

解释

  1. 同步代码依次执行,输出 Start 和 End
  2. 执行微任务队列中的 Promise 1 和 Promise 2
  3. 最后执行宏任务队列中的 setTimeout

常见异步任务类型

任务类型示例
宏任务setTimeoutsetIntervalrequestAnimationFrame, I/O 操作, UI 渲染
微任务Promise.thenMutationObserverqueueMicrotask

关键总结

  1. 同步任务优先执行,异步任务通过队列调度。
  2. 微任务在每轮事件循环的末尾立即执行,宏任务在下一轮循环处理。
  3. 避免阻塞事件循环:长时间运行的同步代码会延迟页面渲染和任务处理。

掌握事件循环机制,可以更精准地控制异步代码的执行顺序,避免常见的陷阱(如 setTimeout 延迟不精确)。