Node.js事件循环机制深度解析
2025.05.28
7 min
2.6k 字
// 目录 · contents
前言
Node.js的核心特性是单线程非阻塞I/O模型,而这一切的基础就是事件循环(Event Loop)。理解事件循环的工作机制,是写出高性能Node.js应用的前提。本文将深入libuv层面,解析事件循环的每个阶段及其执行顺序。
整体架构
graph TB
subgraph "Node.js Architecture"
JS[JavaScript Code] --> V8[V8 Engine]
V8 --> BINDINGS[Node.js Bindings<br>C++ Addons]
BINDINGS --> LIBUV[libuv<br>Event Loop + Thread Pool]
BINDINGS --> OPENSSL[OpenSSL<br>Crypto]
BINDINGS --> ZLIB[zlib<br>Compression]
BINDINGS --> HTTP_PARSER[llhttp<br>HTTP Parser]
LIBUV --> EPOLL[epoll/kqueue/IOCP<br>Async I/O]
LIBUV --> TP[Thread Pool<br>4 threads default]
end
Node.js运行时由几个关键组件构成: - V8:执行JavaScript代码 - libuv:提供事件循环和异步I/O - Node.js Bindings:连接JavaScript和C++层
事件循环的六个阶段
graph TB
START((开始)) --> TIMERS
TIMERS[1. Timers<br>执行setTimeout/setInterval回调] --> PENDING[2. Pending Callbacks<br>执行延迟到下一轮的I/O回调]
PENDING --> IDLE[3. Idle, Prepare<br>内部使用]
IDLE --> POLL[4. Poll<br>检索新I/O事件<br>执行I/O相关回调]
POLL --> CHECK[5. Check<br>执行setImmediate回调]
CHECK --> CLOSE[6. Close Callbacks<br>执行close事件回调<br>如socket.on close]
CLOSE --> |下一轮| TIMERS
style TIMERS fill:#e53935,color:#fff
style POLL fill:#1976d2,color:#fff
style CHECK fill:#388e3c,color:#fff
各阶段详解
1. Timers阶段
执行setTimeout()和setInterval()到期的回调。注意:定时器的时间并不是精确的,而是”至少”等待指定时间。
1 | |
2. Pending Callbacks阶段
处理一些系统操作的回调,比如TCP连接错误。大部分回调在Poll阶段处理,这个阶段只处理少数延迟的I/O回调。
3. Poll阶段
这是最重要的阶段。Poll阶段做两件事: 1. 计算应该阻塞和轮询I/O的时间 2. 处理Poll队列中的事件
flowchart TB
ENTER[进入Poll阶段] --> CHECK_QUEUE{Poll队列<br>有回调?}
CHECK_QUEUE -->|有| EXEC[执行回调<br>直到队列为空<br>或达到系统限制]
CHECK_QUEUE -->|空| CHECK_IMMEDIATE{有setImmediate<br>调度?}
CHECK_IMMEDIATE -->|有| GO_CHECK[进入Check阶段]
CHECK_IMMEDIATE -->|没有| CHECK_TIMER{有到期Timer?}
CHECK_TIMER -->|有| GO_TIMER[进入Timers阶段]
CHECK_TIMER -->|没有| WAIT[等待I/O事件<br>阻塞在这里]
EXEC --> CHECK_QUEUE
4. Check阶段
执行setImmediate()回调。setImmediate是在Poll阶段完成后立即执行。
5. Close Callbacks阶段
处理关闭事件,如socket.on('close', ...)。
微任务与宏任务
graph TB
subgraph "每个阶段之间"
MICRO[微任务队列<br>process.nextTick<br>Promise.then<br>queueMicrotask]
end
subgraph "宏任务(事件循环阶段)"
T[Timers]
P[Pending]
PO[Poll]
C[Check]
CL[Close]
end
T --> |执行所有微任务| MICRO
MICRO --> P
P --> |执行所有微任务| MICRO
MICRO --> PO
PO --> |执行所有微任务| MICRO
MICRO --> C
C --> |执行所有微任务| MICRO
MICRO --> CL
微任务优先级
1 | |
1 | |
setTimeout vs setImmediate
1 | |
sequenceDiagram
participant POLL as Poll Phase
participant CHECK as Check Phase
participant TIMER as Timer Phase
Note over POLL: fs.readFile回调执行
Note over POLL: 调度setTimeout和setImmediate
POLL->>CHECK: 进入Check阶段
Note over CHECK: 执行setImmediate回调
CHECK->>TIMER: 进入Timer阶段
Note over TIMER: 执行setTimeout回调
process.nextTick深入
1 | |
线程池
libuv维护一个线程池(默认4个线程),用于处理无法通过内核异步机制完成的操作。
graph TB
subgraph "Main Thread (Event Loop)"
EL[Event Loop]
end
subgraph "libuv Thread Pool (UV_THREADPOOL_SIZE)"
T1[Thread 1]
T2[Thread 2]
T3[Thread 3]
T4[Thread 4]
end
subgraph "使用线程池的操作"
FS[fs.readFile / writeFile]
DNS[dns.lookup]
CRYPTO[crypto.pbkdf2 / randomBytes]
ZLIB[zlib.gzip / deflate]
end
subgraph "使用内核异步I/O"
NET[net.connect / http.request]
PIPE[pipe操作]
SIGNAL[信号处理]
end
FS --> T1
DNS --> T2
CRYPTO --> T3
ZLIB --> T4
NET --> EL
PIPE --> EL
SIGNAL --> EL
1 | |
实际应用中的注意事项
不要阻塞事件循环
1 | |
监控事件循环延迟
1 | |
1 | |
Worker Threads
对于CPU密集型任务,应该使用Worker Threads而非阻塞事件循环:
1 | |
事件循环完整流程图
flowchart TB
START[Node.js启动] --> INIT[初始化事件循环]
INIT --> EXEC[执行入口脚本<br>同步代码]
EXEC --> MICRO1[处理微任务<br>nextTick > Promise]
MICRO1 --> LOOP_START{有待处理<br>事件/回调?}
LOOP_START -->|否| EXIT[退出进程]
LOOP_START -->|是| TIMERS[Timers阶段]
TIMERS --> MICRO2[微任务]
MICRO2 --> PENDING[Pending阶段]
PENDING --> MICRO3[微任务]
MICRO3 --> POLL[Poll阶段]
POLL --> MICRO4[微任务]
MICRO4 --> CHECK[Check阶段]
CHECK --> MICRO5[微任务]
MICRO5 --> CLOSE[Close阶段]
CLOSE --> MICRO6[微任务]
MICRO6 --> LOOP_START
总结
Node.js事件循环的核心知识点:
- 六个阶段:Timers → Pending → Idle/Prepare → Poll → Check → Close
- 微任务在每个阶段之间执行:
process.nextTick优先级高于Promise.then - Poll阶段是核心:处理I/O事件,控制阻塞等待时间
- setImmediate vs setTimeout(0):在I/O回调中setImmediate总是先执行
- 线程池默认4个线程:处理fs、dns.lookup、crypto等操作,可通过UV_THREADPOOL_SIZE调整
- 不要阻塞事件循环:CPU密集型任务使用Worker Threads
掌握事件循环机制,能帮助你写出更高效的Node.js代码,也能更好地排查性能问题。
$ echo "comments" · 评论