Node.js · #nodejs#event-loop#libuv

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
3
4
5
6
7
8
9
10
11
// setTimeout的执行时机取决于事件循环当前所处阶段
const start = Date.now();

setTimeout(() => {
console.log(`Timer executed after ${Date.now() - start}ms`);
}, 100);

// 模拟一个耗时操作
const data = require('fs').readFileSync('/dev/urandom', { length: 100000 });
// readFileSync是同步的,会阻塞事件循环
// 如果这里耗时200ms,那么setTimeout回调要等到200ms后才执行

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
2
3
4
5
6
7
8
9
10
11
// process.nextTick vs Promise vs queueMicrotask
// process.nextTick优先级最高

Promise.resolve().then(() => console.log('1: Promise'));
queueMicrotask(() => console.log('2: queueMicrotask'));
process.nextTick(() => console.log('3: nextTick'));

// 输出顺序:
// 3: nextTick
// 1: Promise (Promise和queueMicrotask在同一队列)
// 2: queueMicrotask
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
37
38
39
40
// 完整的执行顺序示例
console.log('1: script start');

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

setImmediate(() => {
console.log('4: setImmediate');
process.nextTick(() => {
console.log('5: setImmediate > nextTick');
});
});

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

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

console.log('9: script end');

// 输出:
// 1: script start
// 9: script end
// 7: nextTick
// 8: nextTick > nextTick
// 6: Promise
// 2: setTimeout (Timer和Immediate的顺序在主模块中不确定)
// 3: setTimeout > Promise
// 4: setImmediate
// 5: setImmediate > nextTick

setTimeout vs setImmediate

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在主模块中,顺序不确定(取决于系统性能和事件循环启动时间)
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 可能是 timeout -> immediate,也可能是 immediate -> timeout

// 但在I/O回调中,setImmediate总是先执行
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 总是: immediate -> timeout
// 因为I/O回调在Poll阶段执行,下一个阶段是Check(setImmediate)
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
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
// process.nextTick递归会阻塞事件循环(I/O饥饿)
// 不推荐的写法
function recursiveNextTick() {
process.nextTick(recursiveNextTick);
// 永远不会进入下一个事件循环阶段!
}

// 推荐使用setImmediate替代
function recursiveImmediate() {
setImmediate(recursiveImmediate);
// 每次在Check阶段执行,不会阻塞其他阶段
}

// process.nextTick的合理使用场景
class EventEmitterExample extends require('events') {
constructor() {
super();
// 确保事件处理器在构造函数返回后才触发
process.nextTick(() => {
this.emit('ready');
});
}
}

// 用户可以在构造后注册事件处理器
const emitter = new EventEmitterExample();
emitter.on('ready', () => {
console.log('ready event fired');
});

线程池

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 调整线程池大小
// 在程序启动前设置环境变量
// UV_THREADPOOL_SIZE=16 node app.js

// 或在代码中设置(必须在require任何模块之前)
process.env.UV_THREADPOOL_SIZE = 16;

// 线程池满时的排队行为
const crypto = require('crypto');
const start = Date.now();

// 默认4个线程,5个并发pbkdf2
for (let i = 0; i < 5; i++) {
crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', () => {
console.log(`pbkdf2 ${i}: ${Date.now() - start}ms`);
});
}

// 输出示例(4线程):
// pbkdf2 0: 850ms \
// pbkdf2 1: 855ms > 前4个几乎同时完成
// pbkdf2 2: 860ms /
// pbkdf2 3: 862ms /
// pbkdf2 4: 1710ms -> 第5个等待线程释放,耗时约2倍

实际应用中的注意事项

不要阻塞事件循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 不良实践:同步操作阻塞事件循环
const data = JSON.parse(fs.readFileSync('huge-file.json', 'utf8'));

// 良好实践:使用流或异步操作
const { pipeline } = require('stream/promises');
const { createReadStream } = require('fs');
const JSONStream = require('JSONStream');

async function processLargeJSON(filePath) {
const results = [];
const readStream = createReadStream(filePath);
const parser = JSONStream.parse('*');

parser.on('data', (item) => {
results.push(item);
});

await pipeline(readStream, parser);
return results;
}

监控事件循环延迟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用perf_hooks监控事件循环延迟
const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setInterval(() => {
console.log({
min: histogram.min / 1e6, // 转换为毫秒
max: histogram.max / 1e6,
mean: histogram.mean / 1e6,
p99: histogram.percentile(99) / 1e6,
});
histogram.reset();
}, 5000);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 简单的事件循环延迟检测
function monitorLoop(threshold = 100) {
let lastCheck = Date.now();

setInterval(() => {
const now = Date.now();
const delay = now - lastCheck - 1000; // 预期间隔1000ms
if (delay > threshold) {
console.warn(`Event loop delay: ${delay}ms`);
}
lastCheck = now;
}, 1000).unref(); // unref防止阻止进程退出
}

Worker Threads

对于CPU密集型任务,应该使用Worker Threads而非阻塞事件循环:

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
// main.js
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: data
});
worker.on('message', resolve);
worker.on('error', reject);
});
}

async function main() {
const results = await Promise.all([
runWorker({ task: 'hash', data: 'password1' }),
runWorker({ task: 'hash', data: 'password2' }),
]);
console.log(results);
}

main();
} else {
const { workerData } = require('worker_threads');
const crypto = require('crypto');

const hash = crypto.pbkdf2Sync(
workerData.data, 'salt', 100000, 64, 'sha512'
);
parentPort.postMessage(hash.toString('hex'));
}

事件循环完整流程图

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事件循环的核心知识点:

  1. 六个阶段:Timers → Pending → Idle/Prepare → Poll → Check → Close
  2. 微任务在每个阶段之间执行process.nextTick优先级高于Promise.then
  3. Poll阶段是核心:处理I/O事件,控制阻塞等待时间
  4. setImmediate vs setTimeout(0):在I/O回调中setImmediate总是先执行
  5. 线程池默认4个线程:处理fs、dns.lookup、crypto等操作,可通过UV_THREADPOOL_SIZE调整
  6. 不要阻塞事件循环:CPU密集型任务使用Worker Threads

掌握事件循环机制,能帮助你写出更高效的Node.js代码,也能更好地排查性能问题。

作者 · authorzt
发布 · date2025-05-28
篇幅 · length2.6k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论