003-浏览器与Node事件循环

浏览器与Node.js事件循环(Event Loop)详解

浏览器事件循环(Event Loop)

1. 基本概念

浏览器的事件循环是JavaScript实现异步编程的核心机制,它负责协调用户交互、脚本执行、渲染、网络请求等事件。

2. 运行机制

主要组成部分:

  • **调用栈(Call Stack)**:执行同步代码的栈结构
  • **任务队列(Task Queue)**:
    • 宏任务队列(Macrotask Queue)
    • 微任务队列(Microtask Queue)
  • Web APIs:浏览器提供的异步API环境

执行流程:

  1. 执行全局同步代码(同步任务直接进入调用栈执行)
  2. 遇到异步操作时:
    • 宏任务(setTimeout、setInterval、I/O等)交给Web APIs处理,完成后回调放入宏任务队列
    • 微任务(Promise.then、MutationObserver等)回调放入微任务队列
  3. 当调用栈为空时:
    • 先检查微任务队列并执行所有微任务
    • 然后从宏任务队列取出一个任务执行
  4. 重复上述过程

图示流程:

[执行同步代码] → [微任务队列全部执行] → [渲染(如有需要)] → [取一个宏任务执行] → [重复]

3. 任务分类

宏任务(Macrotasks):

  • script整体代码
  • setTimeout/setInterval
  • I/O操作
  • UI渲染
  • postMessage
  • MessageChannel
  • setImmediate(仅IE/Node.js)

微任务(Microtasks):

  • Promise.then/catch/finally
  • MutationObserver
  • process.nextTick(Node.js特有)
  • queueMicrotask

4. 执行顺序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log('script start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});

console.log('script end');

// 输出顺序:
// script start
// script end
// promise1
// promise2
// setTimeout

Node.js事件循环

1. 基本概念

Node.js使用libuv库实现的事件驱动架构,其事件循环比浏览器更复杂,分为多个阶段。

2. 阶段划分

完整事件循环阶段(按顺序执行):

  1. timers阶段:执行setTimeout和setInterval的回调
  2. pending callbacks阶段:执行某些系统操作的回调(如TCP错误)
  3. idle, prepare阶段:Node内部使用
  4. poll阶段
    • 检索新的I/O事件
    • 执行I/O相关回调(几乎所有的回调,除了close、timers和setImmediate)
    • 可能会阻塞在此阶段等待新事件
  5. check阶段:执行setImmediate的回调
  6. close callbacks阶段:执行关闭事件的回调(如socket.on(‘close’))

3. 特殊队列

nextTick队列

  • process.nextTick()的回调
  • 不属于事件循环的任何阶段
  • 在当前操作完成后立即执行,优先级高于微任务

微任务队列

  • 包括Promise.then、queueMicrotask等
  • 在每个阶段之间执行

4. 执行顺序规则

  1. nextTick队列 > 微任务队列 > 其他阶段
  2. 每个阶段执行完毕后,都会先执行完nextTick队列和微任务队列

5. 与浏览器的区别

  1. 阶段划分不同:Node.js有明确的阶段划分
  2. 优先级不同:
    • Node.js中:process.nextTick > Promise微任务
    • 浏览器中:Promise微任务 > setTimeout宏任务
  3. 实现机制不同:Node.js基于libuv,浏览器基于HTML5规范

6. 执行顺序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
console.log('start');

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

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

Promise.resolve().then(() => {
console.log('promise3');
});

process.nextTick(() => {
console.log('nextTick');
});

console.log('end');

/* Node.js输出顺序:
start
end
nextTick
promise3
timer1
promise1
timer2
promise2
*/

常见面试问题解答

Q1: 浏览器和Node.js事件循环的主要区别?

  1. 阶段划分:Node.js有明确的6个阶段,浏览器更简单
  2. 任务优先级:Node.js中process.nextTick优先级最高
  3. 实现机制:Node.js使用libuv,浏览器遵循HTML5规范
  4. 微任务执行时机:Node.js在每个阶段之间执行微任务

Q2: process.nextTick和setImmediate的区别?

  • process.nextTick()
    • 在当前阶段立即执行
    • 优先级高于微任务
  • setImmediate()
    • 在check阶段执行
    • 属于宏任务

Q3: 为什么Promise.then比setTimeout先执行?

因为Promise.then属于微任务,会在当前宏任务执行完后立即执行所有微任务,而setTimeout是下一个宏任务。

Q4: Node.js中哪些操作会产生微任务?

  • Promise.then/catch/finally
  • async/await
  • queueMicrotask
  • process.nextTick(虽然严格来说不属于微任务,但行为类似且优先级更高)