Node.js 运行时机制
V8 引擎与 libuv 事件循环
Node.js 由两大核心构成:V8 引擎负责 JavaScript 代码的编译执行,libuv 库负责跨平台的异步 I/O。两者通过 Node.js 的 C++ 绑定层连接。
事件循环是 Node.js 的心脏,它将操作分为 6 个阶段,每个阶段维护一个回调队列:
flowchart TD
A[事件循环] --> B["timers<br/>执行 setTimeout/setInterval 回调"]
B --> C["pending callbacks<br/>系统级回调(TCP 错误等)"]
C --> D["idle, prepare<br/>libuv 内部使用"]
D --> E["poll<br/>检索新 I/O 事件,执行 I/O 回调"]
E --> F["check<br/>执行 setImmediate 回调"]
F --> G["close callbacks<br/>关闭事件回调(socket.on('close'))"]
G --> B
H["nextTickQueue<br/>process.nextTick"] -.->|每个阶段之间| I["microTaskQueue<br/>Promise.then"]
六个阶段详解
| 阶段 | 执行内容 | 典型 API |
|---|---|---|
| timers | 到期的定时器回调 | setTimeout, setInterval |
| pending callbacks | 系统操作的回调 | TCP 连接错误回调 |
| idle, prepare | libuv 内部 | — |
| poll | I/O 事件和回调 | 文件读取、网络请求完成回调 |
| check | 立即执行的回调 | setImmediate |
| close callbacks | 关闭事件 | socket.destroy() 的回调 |
关键细节:
process.nextTick不在事件循环的任何阶段,而是在每个阶段之间执行,且优先于微任务队列setImmediate在 poll 阶段之后执行,setTimeout(0)在 timers 阶段执行,但在非 I/O 上下文中顺序不确定- poll 阶段如果队列为空:有
setImmediate则进入 check 阶段;有定时器则等待到时间后进入 timers 阶段;否则阻塞等待 I/O
模块系统
CommonJS 加载机制
// 导出
module.exports = { add, subtract };
// 或
exports.add = function(a, b) { return a + b; };
// 导入
const math = require('./math');
CommonJS 的加载是同步的,require() 执行时会:
- 解析模块路径为绝对路径
- 检查缓存(
require.cache),已加载则直接返回module.exports - 未缓存则创建
module对象,执行模块代码 - 缓存并返回
module.exports
这意味着 CommonJS 模块可以条件加载和动态加载:
if (process.env.NODE_ENV === 'production') {
const monitoring = require('./monitoring'); // 条件加载
}
ESM(ECMAScript Modules)
// 导出
export function add(a, b) { return a + b; }
export default class Calculator {}
// 导入
import Calculator, { add } from './math.js';
ESM 与 CommonJS 的关键差异:
| 特性 | CommonJS | ESM |
|---|---|---|
| 加载方式 | 运行时同步加载 | 编译时静态分析 |
| 导出值 | 值的拷贝 | 值的引用(绑定) |
| this 顶层 | module.exports |
undefined |
| 循环依赖 | 得到已执行部分的快照 | 得到引用,可能未初始化 |
| Tree-shaking | 不支持 | 支持 |
// CJS: 值的拷贝
// counter.js
let count = 0;
module.exports = { count, increment: () => ++count };
// main.js
const { count, increment } = require('./counter');
console.log(count); // 0
increment();
console.log(count); // 0 — count 是拷贝,不会更新
// ESM: 值的引用
// counter.mjs
export let count = 0;
export function increment() { ++count; }
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 — count 是引用,实时更新
流(Stream)处理
流是 Node.js 处理大数据的核心抽象,数据分块传输而非一次性加载到内存:
flowchart LR
A[可读流<br/>Readable] -->|pipe| B[转换流<br/>Transform]
B -->|pipe| C[可写流<br/>Writable]
D[双工流<br/>Duplex<br/>读写独立] --> E[如: TCP Socket]
四种流类型:
- Readable:可读流(
fs.createReadStream、HTTP 请求体) - Writable:可写流(
fs.createWriteStream、HTTP 响应体) - Duplex:双工流,读写独立(TCP Socket)
- Transform:转换流,读入数据经变换后输出(
zlib.createGzip)
背压机制
当生产者速度 > 消费者速度时,数据会堆积在内存中。背压(Backpressure)机制通过反馈信号让生产者暂停:
const fs = require('fs');
const zlib = require('zlib');
// ❌ 不处理背压
readable.on('data', (chunk) => {
writable.write(chunk); // 内部缓冲区可能溢出
});
// ✅ pipe 自动处理背压
fs.createReadStream('bigfile.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('bigfile.txt.gz'));
// ✅ 手动处理背压
const source = fs.createReadStream('bigfile.txt');
const dest = fs.createWriteStream('copy.txt');
source.on('data', (chunk) => {
const canContinue = dest.write(chunk);
if (!canContinue) {
source.pause(); // 写入缓冲区满,暂停读取
}
});
dest.on('drain', () => {
source.resume(); // 缓冲区排空,恢复读取
});
Cluster 与 Worker Threads
Cluster:多进程
Cluster 利用多核 CPU,主进程(Master)监听端口,通过 IPC 将连接分发给工作进程(Worker):
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isPrimary) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // 自动重启
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello from worker ' + process.pid);
}).listen(3000);
}
Worker Threads:多线程
Worker Threads 共享进程内存,适合 CPU 密集型任务:
const { Worker, isMainThread, parentPort, SharedArrayBuffer } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (result) => console.log('Result:', result));
worker.postMessage({ data: 'compute this' });
} else {
parentPort.on('message', (msg) => {
const result = heavyComputation(msg.data);
parentPort.postMessage(result);
});
}
| 特性 | Cluster | Worker Threads |
|---|---|---|
| 隔离性 | 进程级隔离 | 线程级,共享内存 |
| 通信方式 | IPC(序列化) | MessagePort / SharedArrayBuffer |
| 适用场景 | HTTP 服务多核扩展 | CPU 密集型计算 |
| 内存开销 | 每个进程独立 V8 实例 | 共享主进程 V8 |
内存管理
V8 的垃圾回收基于分代回收:
- 新生代(Young Generation):存放短生命周期对象,使用 Scavenge(半空间复制)算法,GC 频繁但速度快
- 老生代(Old Generation):存放长期存活对象,使用 Mark-Sweep / Mark-Compact,GC 较少但暂停时间长
常见内存泄漏场景:
// 1. 闭包引用
function createLeak() {
const bigData = new Array(1000000);
return function() {
return bigData.length; // bigData 无法被回收
};
}
// 2. 全局变量
function handler(req, res) {
leakedData = req.body; // 忘记 var/let/const
}
// 3. 事件监听器未移除
emitter.on('event', callback);
// 忘记 emitter.removeListener('event', callback);
// 4. 缓存无限增长
const cache = new Map();
app.get('/data/:id', (req, res) => {
if (!cache.has(req.params.id)) {
cache.set(req.params.id, fetchData(req.params.id)); // 只增不减
}
res.json(cache.get(req.params.id));
});
排查工具:process.memoryUsage() 查看堆内存使用,--inspect 配合 Chrome DevTools 做堆快照对比,node --max-old-space-size=4096 调整老生代上限。
评论