浏览器事件循环机制

1. 为什么需要事件循环?

JavaScript 是单线程的。为了防止耗时任务(如网络请求)阻塞主线程,导致页面卡死,JS 采用了非阻塞的异步模型

事件循环 (Event Loop) 就是实现这一模型的机制,它负责协调代码执行、事件处理和任务队列。

2. 核心组成部分

  1. 调用栈 (Call Stack): 执行同步代码的地方。当函数被调用时入栈,执行完出栈。
  2. Web APIs: 浏览器提供的能力(DOM, AJAX, setTimeout 等)。当异步操作开始时,会被移出调用栈,交给 Web APIs 处理。
  3. 任务队列 (Task Queue): 存放异步操作完成后的回调函数。
    • 宏任务队列 (Macrotask Queue)
    • 微任务队列 (Microtask Queue)

3. 事件循环流程 (非常重要)

  1. 执行栈选择最先进入队列的宏任务(通常是整体代码 script),执行其同步代码。
  2. 执行过程中,如果遇到异步操作,将其回调放入对应的宏任务队列微任务队列
  3. 当当前宏任务执行完毕(调用栈清空)后,立即检查微任务队列
  4. 清空微任务队列:依次执行所有微任务,直到队列为空。(如果在执行微任务过程中产生了新的微任务,也会在当前循环中被执行,这可能导致无限循环阻塞)。
  5. UI 渲染:浏览器尝试进行页面渲染(如果需要)。
  6. 下一轮循环:从宏任务队列中取出下一个宏任务,回到步骤 1。

口诀一宏一清微,渲染再轮回

4. 宏任务 vs 微任务

类型常见 API
宏任务 (Macrotask)script (整体代码), setTimeout, setInterval, setImmediate (Node), I/O, UI Rendering
微任务 (Microtask)Promise.then/catch/finally, process.nextTick (Node), MutationObserver, queueMicrotask

5. 经典面试题解析

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

process.nextTick(function() {
    console.log('6');
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

(注:process.nextTick 是 Node 环境特有,优先级高于 Promise)

浏览器环境简易版输出预测

  1. 同步: 1, 7
  2. 微任务: 8
  3. 宏任务1 (setTimeout): 2, 4 -> 微任务: 5
  4. 宏任务2 (setTimeout): 9, 11 -> 微任务: 12

(Node 环境稍有不同,且 Node 版本不同表现也不同,面试通常以浏览器为主)。

6. Vue.nextTick 原理

Vue 的 DOM 更新是异步的。当你修改数据后,DOM 不会立即更新,而是开启一个队列。 Vue.nextTick 的原理就是利用微任务(优先使用 Promise.then,降级使用 MutationObserversetImmediate, setTimeout),在 DOM 更新循环结束之后执行延迟回调。