一、libuv事件循环的核心阶段(六大阶段)
libuv事件循环是Node.js实现非阻塞I/O的底层核心,按固定顺序执行各阶段任务,每个阶段对应专属回调队列:
1. timers(定时器阶段)
- 核心作用:执行
setTimeout/setInterval的到期回调函数; - 关键细节:定时器回调有专属队列,优先级高于I/O回调,
setTimeout(fn, 0)并非立即执行,需等待本轮timers阶段或下轮循环;
示例:
setTimeout(() => {
console.log('timers阶段执行');
}, 0);
2. pending callbacks(待处理回调阶段)
- 核心作用:执行上一轮事件循环中延迟到当前轮次的系统操作回调;
- 典型场景:TCP连接失败的错误回调、部分操作系统异步操作的延迟回调;
示例:
// 模拟TCP连接失败的回调会进入该阶段
const net = require('net');
const client = net.connect({ port: 12345 }, () => {});
client.on('error', (err) => {
console.log('pending callbacks阶段执行'); // 连接失败回调在此阶段触发
});
3. idle/prepare(空闲/准备阶段)
- 核心作用:libuv内部使用阶段,开发者无需关注;
- 细节:idle用于计算事件循环空闲时间,prepare为poll阶段做准备工作;
4. poll(轮询阶段)
- 核心作用:处理I/O事件回调(文件/网络请求等),是事件循环的核心阶段;
- 执行逻辑:
- 若poll队列非空:同步执行队列中的I/O回调,直到队列为空或达到系统限制;
- 若poll队列为空:
- 存在
setImmediate回调 → 直接进入check阶段; - 无
setImmediate回调 → 等待新I/O事件加入队列,若有timer到期则跳回timers阶段;
- 存在
示例:
const fs = require('fs');
fs.readFile('./test.txt', () => {
console.log('poll阶段执行I/O回调'); // 文件读取完成回调在此阶段触发
});
5. check(检查阶段)
- 核心作用:专门执行
setImmediate的回调函数; - 执行时机:poll阶段结束后立即触发,优先级高于下一轮timers阶段(若在I/O回调内同时存在
setTimeout和setImmediate);
示例:
fs.readFile('./test.txt', () => {
setTimeout(() => console.log('timers'), 0);
setImmediate(() => console.log('check')); // 优先执行
});
6. close callbacks(关闭回调阶段)
- 核心作用:处理I/O资源关闭的回调函数;
- 典型场景:Socket连接断开、文件描述符关闭、
stream.destroy()的回调;
示例:
const net = require('net');
const server = net.createServer();
server.on('close', () => {
console.log('close callbacks阶段执行'); // 服务器关闭回调在此阶段触发
});
server.close();
二、libuv的观察者类型及作用
观察者是libuv监听事件的“传感器”,负责将事件回调分发到对应阶段队列,主要包含五类:
1. 定时器观察者
- 监听对象:
setTimeout/setInterval的到期事件; - 触发阶段:timers阶段;
- 核心逻辑:维护定时器队列,按到期时间排序,到期后将回调放入timers队列。
2. I/O观察者
- 监听对象:文件/网络描述符的I/O事件(如可读/可写);
- 触发阶段:poll阶段;
- 核心逻辑:基于epoll/kqueue(不同系统)实现I/O多路复用,监听事件就绪后触发回调。
3. 检查观察者
- 监听对象:
setImmediate的调用事件; - 触发阶段:check阶段;
- 核心逻辑:
setImmediate调用时注册观察者,poll阶段结束后触发回调。
4. 空闲观察者
- 监听对象:事件循环的空闲状态;
- 触发阶段:idle阶段;
- 核心逻辑:事件循环空闲时触发,用于低优先级背景任务(如内存清理)。
5. 准备观察者
- 监听对象:poll阶段的准备信号;
- 触发阶段:prepare阶段;
- 核心逻辑:poll阶段执行前触发,用于初始化I/O轮询的参数配置。
三、Node.js与浏览器事件循环的核心差异
| 特性维度 | Node.js事件循环(libuv) | 浏览器事件循环 |
|---|---|---|
| 底层实现 | 基于libuv库(C语言),分6个阶段 | 基于浏览器内核(Blink/WebKit),分宏/微任务 |
| 微任务执行时机 | 每个回调结束后执行(process.nextTick优先级最高) | 每个宏任务执行后清空所有微任务 |
| 核心关注场景 | 后端I/O处理(文件/网络) | 前端渲染(重绘/回流)+ 用户交互 |
| 特殊API支持 | setImmediate/process.nextTick | requestAnimationFrame/MutationObserver |
| 定时器精度 | 受事件循环阶段影响,精度约1ms | 受浏览器渲染帧率影响,精度约4ms |
示例对比:
// Node.js环境:先执行nextTick(微任务)→ timers → check
console.log('同步代码');
process.nextTick(() => console.log('nextTick微任务'));
setTimeout(() => console.log('timers阶段'), 0);
setImmediate(() => console.log('check阶段'));
// 浏览器环境:先执行Promise(微任务)→ setTimeout(宏任务)
console.log('同步代码');
Promise.resolve().then(() => console.log('Promise微任务'));
setTimeout(() => console.log('宏任务setTimeout'), 0);
四、经典面试题解析:I/O回调内的执行顺序
题目:分析以下代码输出顺序
const fs = require('fs');
// 场景1:I/O回调内的setTimeout vs setImmediate
fs.readFile('./test.txt', () => {
setTimeout(() => console.log('1: setTimeout'), 0);
setImmediate(() => console.log('1: setImmediate'));
});
// 场景2:同步代码中的setTimeout vs setImmediate
setTimeout(() => console.log('2: setTimeout'), 0);
setImmediate(() => console.log('2: setImmediate'));
解析:
场景1输出:
1: setImmediate→1: setTimeout- I/O回调在poll阶段执行,执行完后poll队列为空,直接进入check阶段执行
setImmediate; setTimeout需等待下一轮timers阶段执行。
- I/O回调在poll阶段执行,执行完后poll队列为空,直接进入check阶段执行
场景2输出:顺序不确定
- 同步代码执行时,
setTimeout可能先进入timers队列,也可能setImmediate先进入check队列; - 取决于事件循环启动时timer是否到期,因此顺序不固定。
- 同步代码执行时,
五、libuv事件循环的本质(伪代码)
// libuv事件循环核心逻辑伪代码
while (eventLoop.isAlive()) {
// 1. timers阶段:执行到期定时器回调
timersPhase.execute();
// 2. pending callbacks阶段:执行延迟系统回调
pendingCallbacksPhase.execute();
// 3. idle/prepare阶段:内部准备工作
idlePhase.execute();
preparePhase.execute();
// 4. poll阶段:处理I/O事件,可能阻塞等待
const timeout = pollPhase.getTimeout();
pollPhase.waitForIOEvents(timeout);
pollPhase.execute();
// 5. check阶段:执行setImmediate回调
checkPhase.execute();
// 6. close callbacks阶段:执行关闭回调
closeCallbacksPhase.execute();
// 执行所有微任务(nextTick/Promise)
microtasks.execute();
}
Node.js 事件循环中的微任务执行时机
核心结论
微任务在每个宏任务(回调函数)执行完毕后立即执行,而不是等整个阶段或整个事件循环结束。
事件循环的 6 个阶段
┌───────────────────────────┐
┌─>│ timers │ 执行 setTimeout/setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 执行 I/O 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ 获取新的 I/O 事件
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ 执行 setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │ 执行 close 事件
│ └─────────────┬─────────────┘
└──────────────────────────────┘
微任务执行时机详解
规则
每执行完一个宏任务(回调),就立即清空微任务队列。
示例
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(() => console.log('promise2'));
}, 0);
// 输出:
// timer1
// promise1 ← timer1 回调执行完,立即执行其微任务
// timer2
// promise2 ← timer2 回调执行完,立即执行其微任务
执行流程
timers 阶段:
1. 执行 timer1 回调 → console.log('timer1')
2. 清空微任务队列 → console.log('promise1')
3. 执行 timer2 回调 → console.log('timer2')
4. 清空微任务队列 → console.log('promise2')
常见误解 ❌
- ❌ 微任务在整个阶段的所有回调执行完才执行
- ❌ 微任务在整个事件循环(6个阶段)结束才执行
- ❌ timers 阶段的所有 setTimeout 都执行完才执行微任务
正确理解 ✅
- ✅ 每个宏任务执行完 → 立即清空微任务队列
- ✅ 微任务优先级高于下一个宏任务
- ✅ 这确保了 Promise 的高优先级特性
微任务类型
Promise.then/catch/finallyprocess.nextTick(优先级更高,在微任务之前)queueMicrotask()MutationObserver
宏任务类型
setTimeoutsetIntervalsetImmediate- I/O 操作
- UI 渲染(浏览器)
process.nextTick 特殊性
setTimeout(() => {
console.log('timer');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
}, 0);
// 输出:
// timer
// nextTick ← nextTick 优先于 Promise
// promise
process.nextTick 优先级高于普通微任务,在微任务队列之前执行。
关键记忆点
宏任务 → 微任务 → 宏任务 → 微任务 → ...
每执行一个宏任务,就清空一次微任务队列,然后才执行下一个宏任务。
graph TB
Start([开始新的事件循环]) --> Timers[Timers 阶段]
Timers --> TimerCallback{有 timer 回调?}
TimerCallback -->|是| ExecTimer[执行一个 timer 回调]
ExecTimer --> ClearMicro1[清空微任务队列]
ClearMicro1 --> TimerCallback
TimerCallback -->|否| Pending[Pending Callbacks 阶段]
Pending --> PendingCallback{有 pending 回调?}
PendingCallback -->|是| ExecPending[执行一个 pending 回调]
ExecPending --> ClearMicro2[清空微任务队列]
ClearMicro2 --> PendingCallback
PendingCallback -->|否| Idle[Idle/Prepare 阶段]
Idle --> Poll[Poll 阶段]
Poll --> PollCallback{有 I/O 回调?}
PollCallback -->|是| ExecPoll[执行一个 I/O 回调]
ExecPoll --> ClearMicro3[清空微任务队列]
ClearMicro3 --> PollCallback
PollCallback -->|否| Check[Check 阶段]
Check --> CheckCallback{有 setImmediate 回调?}
CheckCallback -->|是| ExecCheck[执行一个 setImmediate 回调]
ExecCheck --> ClearMicro4[清空微任务队列]
ClearMicro4 --> CheckCallback
CheckCallback -->|否| Close[Close Callbacks 阶段]
Close --> CloseCallback{有 close 回调?}
CloseCallback -->|是| ExecClose[执行一个 close 回调]
ExecClose --> ClearMicro5[清空微任务队列]
ClearMicro5 --> CloseCallback
CloseCallback -->|否| Continue{继续循环?}
Continue -->|是| Timers
Continue -->|否| End([结束])
style ClearMicro1 fill:#90EE90
style ClearMicro2 fill:#90EE90
style ClearMicro3 fill:#90EE90
style ClearMicro4 fill:#90EE90
style ClearMicro5 fill:#90EE90
style ExecTimer fill:#FFE4B5
style ExecPending fill:#FFE4B5
style ExecPoll fill:#FFE4B5
style ExecCheck fill:#FFE4B5
style ExecClose fill:#FFE4B5