前端JS: JavaScript 事件循环

22 阅读2分钟

事件循环机制的核心是协调 JavaScript 单线程运行时,处理异步任务和事件回调的执行顺序,使其不阻塞主线程。

关键要点:

  1. 单线程 + 异步:JavaScript 是单线程的,但通过事件循环机制,能够“非阻塞”地处理 I/O 等异步操作。

  2. 两个队列

    • 宏任务队列 (MacroTask Queue / Task Queue) :包含整体 script 代码、setTimeoutsetInterval、I/O 操作、UI 渲染、setImmediate(Node.js)、requestAnimationFrame(浏览器) 等。
    • 微任务队列 (MicroTask Queue / Job Queue) :包含 Promise.then/catch/finallyprocess.nextTick(Node.js)、MutationObserver(浏览器)、queueMicrotask等。微任务的优先级高于宏任务
  3. 执行流程 (浏览器环境简化版)

    • 同步代码执行:首先执行全局 script 代码(这是一个宏任务)。执行过程中,遇到同步代码立即执行,遇到异步 API 则根据类型处理:

      • 宏任务:将其回调推入宏任务队列。
      • 微任务:将其回调推入微任务队列。
    • 清空微任务:当前宏任务执行完毕后,会立即、依次、彻底地清空当前微任务队列中的所有任务(包括在执行这些微任务过程中新产生的微任务,也会继续执行,直到队列为空)。

    • 渲染 (如有需要) :如果浏览器需要更新视图,会执行 UI 渲染。

    • 取下一个宏任务:从宏任务队列中取出一个任务执行,然后重复“清空微任务 -> (可能渲染) -> 取下一个宏任务”的循环。

一个经典的面试题例子:

console.log('1'); // 同步代码

setTimeout(() => {
  console.log('2'); // 宏任务回调
  Promise.resolve().then(() => {
    console.log('3'); // 微任务回调
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4'); // 微任务回调
  setTimeout(() => {
    console.log('5'); // 宏任务回调
  }, 0);
});

console.log('6'); // 同步代码

输出顺序:1, 6, 4, 2, 3, 5

拆解步骤:

  1. 执行全局 script(宏任务1):打印 16
  2. 遇到 setTimeout,回调函数推入宏任务队列。
  3. 遇到 Promise.resolve().then,回调函数推入微任务队列。
  4. 当前宏任务执行完毕,立即清空微任务队列:执行微任务,打印 4,同时遇到内部的 setTimeout,其回调推入宏任务队列。
  5. 取下一个宏任务(来自第2步的 setTimeout回调,宏任务2):打印 2,遇到内部的 Promise.then,其回调推入微任务队列。
  6. 关键每个宏任务执行完毕后,都要立即清空微任务队列。所以,此时立即执行刚加入的微任务,打印 3
  7. 取下一个宏任务(来自第4步的 setTimeout回调,宏任务3):打印 5

问: 介绍一下 JavaScript 事件循环

答:

  • JavaScript 是单线程的,事件循环使其能处理异步。
  • 任务分为宏任务和微任务,微任务优先级更高。
  • 主线程(同步代码)执行 -> 清空所有微任务 -> 取一个宏任务执行 -> 再清空所有微任务 -> ... 如此循环。
  • 在浏览器中,每次事件循环可能伴随着 UI 渲染。