浏览器事件循环详解

1. 核心原理

JavaScript 是单线程的,这使得它无法同时执行多个任务。如果不使用异步机制,IO 操作(网络请求、定时器)会导致页面卡死。

Event Loop 是 JavaScript 运行时的并发模型。它负责协调调用栈 (Call Stack)(同步任务)与任务队列 (Task Queue)(异步回调)之间的执行顺序。

2. 宏任务 (Macrotask) 与 微任务 (Microtask)

这是面试中最关键的点。两者的执行时机完全不同。

(1) 宏任务 (Macrotask Queue)

  • 来源:浏览器宿主环境发起的任务。
  • 包含
    • 整体代码 script (首次运行)。
    • setTimeout
    • setInterval
    • setImmediate (Node.js/IE)。
    • I/O 操作。
    • UI Rendering (浏览器渲染)。

(2) 微任务 (Microtask Queue)

  • 来源:JavaScript 引擎自身发起的任务 (Promise)。
  • 包含
    • Promise.then/catch/finally
    • MutationObserver (DOM 变动监听)。
    • queueMicrotask
    • process.nextTick (Node.js 特有,优先级最高)。

3. 执行顺序 (Loop Process)

  1. 执行同步代码:首先执行当前宏任务(通常是整体 script),直到调用栈清空。
  2. 清空微任务队列:一旦当前宏任务执行完毕,立即检查微任务队列。如果有微任务,依次执行,直到清空为止
    • 注意:如果在执行微任务的过程中又产生了新的微任务,会继续加入队尾并执行(可能导致无限循环阻塞)。
  3. UI 渲染:如果浏览器需要更新页面(通常 16ms 一次),此时会进行。
  4. 执行下一个宏任务:从宏任务队列取出一个任务,回到第一步。

简化口诀一宏 -> 清微 -> 渲染 -> 下一宏

4. 代码实战 (面试必考)

console.log('1');

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

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

console.log('6');

解析步骤

  1. 宏任务 (Script)

    • 打印 1
    • 遇见 setTimeout,回调放入 Macrotask Queue
    • 遇见 new Promise,构造函数同步执行 -> 打印 4
    • resolve().then 回调放入 Microtask Queue
    • 打印 6
    • 宏任务结束。
  2. 微任务 (Microtask Queue)

    • 取出 Promise.then 回调 -> 打印 5
    • 队列清空。
  3. 渲染 (略)。

  4. 宏任务 (Macrotask Queue)

    • 取出 setTimeout 回调 -> 打印 2
    • 遇见 Promise.then,放入 Microtask Queue
    • 宏任务结束。
  5. 微任务 (Microtask Queue)

    • 取出新产生的微任务 -> 打印 3

最终输出1 -> 4 -> 6 -> 5 -> 2 -> 3

5. Node.js 与 浏览器的区别

在 Node 11 之前,Node 会一次执行完所有同类型的宏任务(比如所有的 timer),再清空微任务。 但在 Node 11 之后,Node 的行为已经修改为与浏览器一致:每执行一个宏任务,就清空一次微任务

6. 面试常见问题

Q:setTimeout 准时吗?

不准时。它只能保证延时最小时间。如果主线程被阻塞(例如计算密集型任务),或者宏任务队列前面有很多任务,setTimeout 的执行时间会被推迟。

Q:requestAnimationFrame 是宏任务吗?

它既不是宏任务也不是微任务。它是在UI 渲染之前执行的回调,用于执行动画。它的执行频率跟随屏幕刷新率(通常 60Hz)。

Q: 为什么 Promise 比 setTimeout 快?

因为 Promise 是微任务,在当前宏任务结束后立即执行。而 setTimeout 需要等待至少下一次 Event Loop。