Node.js 核心
前言
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它让 JavaScript 跳出浏览器限制,成为服务端开发的全能语言。与浏览器环境不同,Node.js 提供了文件系统操作、网络通信、进程管理等底层能力,同时保留了事件驱动、非阻塞 I/O 的核心设计哲学。本文档从模块系统、事件循环、Stream、Buffer、Cluster 等核心维度展开,帮助开发者系统掌握 Node.js。
一、Node.js 模块系统
1.1 CommonJS 规范
Node.js 最初采用 CommonJS 模块规范,通过 require 加载模块,通过 module.exports 或 exports 导出模块。每个文件在 Node.js 中都被视为一个独立的模块,运行时会被包裹在一个函数中,实现作用域隔离。
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = { add, subtract };
// app.js
const math = require('./math.js');
console.log(math.add(2, 3)); // 5CommonJS 的模块加载机制包含以下步骤:
- 路径解析:按路径规则查找文件,支持相对路径、绝对路径、node_modules 查找。
- 文件定位:尝试补全扩展名(.js → .json → .node),若为目录则查找 index 文件。
- 编译执行:读取文件内容后,用函数包裹器包裹并执行。
- 缓存:模块首次加载后被缓存,后续
require直接返回缓存结果。
函数包裹器示意:
(function(exports, require, module, __filename, __dirname) {
// 模块代码实际在此执行
});1.2 ES Module(ESM)
Node.js 从 12.x 开始稳定支持 ES Module。有两种方式启用 ESM:文件使用 .mjs 扩展名,或在 package.json 中设置 "type": "module"。
// utils.mjs
export const formatDate = (date) => date.toISOString();
export const parseJSON = (str) => JSON.parse(str);
// main.mjs
import { formatDate, parseJSON } from './utils.mjs';ESM 与 CommonJS 的关键区别:
| 特性 | CommonJS | ES Module |
|---|---|---|
| 加载方式 | 同步 | 异步 |
| 语法 | require / module.exports | import / export |
| 静态分析 | 不支持 | 支持(Tree Shaking 基础) |
| 循环依赖 | 返回未完成 exports | 依赖静态结构更稳定 |
| 顶层 await | 不支持 | 模块顶层支持 |
1.3 模块加载算法
Node.js 的模块加载遵循一套精密的查找算法:
- 内置模块优先:如
fs、path、http等,即使 node_modules 中存在同名包也不会被加载。 - 相对/绝对路径:
./或../开头的路径按文件路径查找。 - node_modules 逐级查找:从当前目录的
node_modules向上查找直至根目录。 - NODE_PATH 环境变量:如果以上均未找到,则按
NODE_PATH环境变量中的路径查找。
二、Node.js 事件循环
事件循环(Event Loop)是 Node.js 非阻塞 I/O 模型的核心。它允许 Node.js 在单线程上处理成千上万的并发连接,而不会阻塞主线程。
2.1 事件循环的六个阶段
Node.js 的事件循环分为六个阶段(Phase),每个阶段都有一个 FIFO 回调队列:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │
│ └───────────────────────────┘1. timers(定时器阶段)
执行由 setTimeout() 和 setInterval() 设置的回调。定时器的延迟时间并非精确保证——它只是"最早执行时间",实际执行取决于事件循环到达该阶段时队列的状态。
setTimeout(() => {
console.log('延迟 10ms 执行');
}, 10);2. pending callbacks(待定回调阶段)
执行延迟到下一轮循环的 I/O 回调。例如 TCP 连接错误等系统级别的回调会在此阶段处理。
3. idle, prepare(空闲/准备阶段)
仅供系统内部使用,开发者一般无需关注。
4. poll(轮询阶段)
这是事件循环中最重要的阶段。此阶段会执行以下操作:
- 如果 poll 队列不为空,则同步执行所有回调。
- 如果 poll 队列为空,则检查是否有
setImmediate()回调(进入 check 阶段)。如果没有其他待处理任务,事件循环将在此等待新的 I/O 事件。
5. check(检查阶段)
专门执行 setImmediate() 设置的回调。setImmediate() 的回调会在 poll 阶段结束后立即执行。
6. close callbacks(关闭回调阶段)
执行关闭事件的回调,如 socket.on('close', ...)。
2.2 microtask 与 process.nextTick
事件循环的六个阶段之外,还有两个重要的任务队列:
- microtask(微任务):包括 Promise.then/catch/finally、queueMicrotask、MutationObserver。在每个阶段切换时清空微任务队列。
- process.nextTick:虽然名字包含"nextTick",但它不属于事件循环的任何一个阶段,而是在每个阶段切换时、以及每个回调执行后都会优先执行 Next Tick 队列中的所有任务。
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// 输出顺序:nextTick → Promise → setTimeout → setImmediateTIP
process.nextTick 的优先级高于 Promise 微任务,建议优先使用 queueMicrotask 或 Promise 代替 process.nextTick,以保持与浏览器环境的一致性。
2.3 事件循环示例
const fs = require('fs');
fs.readFile(__filename, () => {
console.log('I/O callback (poll)');
setTimeout(() => console.log('setTimeout'));
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
});
// 输出顺序:I/O callback → nextTick → setImmediate → setTimeout三、Stream(流)
Stream 是 Node.js 中处理流式数据的抽象接口。它允许以分块的方式处理数据,无需一次性将整个数据集加载到内存中,非常适合大文件处理、网络通信等场景。
3.1 Stream 的四种类型
| 类型 | 说明 | 方向 | 关键方法/事件 |
|---|---|---|---|
| Readable(可读流) | 数据来源,可读取数据 | 读取 | data 事件、readable 事件、read()、pipe() |
| Writable(可写流) | 数据目标,可写入数据 | 写入 | write()、end()、drain 事件、finish 事件 |
| Transform(转换流) | 既可读又可写,中间转换数据 | 双向 | 继承自 Duplex,需实现 _transform 方法 |
| Duplex(双工流) | 既可读又可写,但读写独立 | 双向 | 组合 Readable 和 Writable |
3.2 Readable Stream(可读流)
可读流有两种读取模式:
- 流动模式(Flowing):数据自动从底层系统读取,通过
data事件推送给消费者。 - 暂停模式(Paused):必须显式调用
read()方法从流中拉取数据。
const { Readable } = require('stream');
// 创建自定义可读流
const readable = new Readable({
highWaterMark: 16 * 1024, // 每次读取 16KB
read(size) {
// 向流中推送数据
this.push(Math.random() > 0.5 ? null : 'data chunk\n');
}
});
// 流动模式
readable.on('data', (chunk) => {
console.log('收到数据块:', chunk.toString());
});
readable.on('end', () => {
console.log('数据读取完毕');
});3.3 Writable Stream(可写流)
可写流接收数据并将其写入目标。关键方法是 write() 和 end(),drain 事件在内部缓冲区清空后触发,用于实现背压控制。
const { Writable } = require('stream');
const writable = new Writable({
highWaterMark: 16 * 1024,
write(chunk, encoding, callback) {
// 处理写入的数据
console.log('写入数据:', chunk.toString());
callback(); // 处理完成后调用
}
});
// 写入数据
writable.write('Hello ');
writable.write('World!');
writable.end(); // 结束写入,触发 finish 事件3.4 Transform Stream(转换流)
转换流是最常用也最强大的流类型,它在数据流经时对数据进行修改或转换。
const { Transform } = require('stream');
// 创建一个大写转换流
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
// 将接收到的数据块转换为大写
const transformed = chunk.toString().toUpperCase();
this.push(transformed);
callback();
}
});
// 管道连接:读取 → 转换 → 写入
readable.pipe(upperCaseTransform).pipe(writable);3.5 Duplex Stream(双工流)
双工流同时实现了 Readable 和 Writable 接口,读写两端相互独立。典型的例子是 TCP Socket——可以读取远端数据,也可以写入数据发送给远端。
const { Duplex } = require('stream');
const duplex = new Duplex({
read(size) {
this.push('read data\n');
this.push(null); // 结束读取
},
write(chunk, encoding, callback) {
console.log('写入端收到:', chunk.toString());
callback();
}
});3.6 pipeline 管道
pipeline 是 Node.js 推荐用于连接多个流的 API,它能够自动处理流销毁、错误传播和清理工作。
const { pipeline, Transform } = require('stream');
const fs = require('fs');
const gzipTransform = new Transform({
transform(chunk, encoding, callback) {
// 模拟压缩处理
this.push(chunk);
callback();
}
});
pipeline(
fs.createReadStream('input.txt'),
gzipTransform,
fs.createWriteStream('output.txt'),
(err) => {
if (err) {
console.error('管道处理失败:', err);
} else {
console.log('管道处理完成');
}
}
);WARNING
pipeline 已取代旧的 pipe() 方法成为官方推荐方案,因为它能正确处理背压和流销毁。在 Node.js 15.0 之后,pipeline 也支持 Promise 版本(stream.promises.pipeline)。
四、Buffer(缓冲区)
Buffer 是 Node.js 中用于处理二进制数据的类。在 ECMAScript 2015(ES6)之前,JavaScript 没有处理二进制数据的机制,Node.js 因此引入了 Buffer。现在,Buffer 的实例属于 Uint8Array 的子类。
4.1 创建 Buffer
// 分配长度为 10 字节的缓冲区
const buf1 = Buffer.alloc(10);
// 分配长度为 10 字节的缓冲区,初始填充为 1
const buf2 = Buffer.alloc(10, 1);
// 从数组创建
const buf3 = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
// 从字符串创建(默认 utf8 编码)
const buf4 = Buffer.from('Hello Node.js', 'utf-8');
// Buffer.allocUnsafe(性能更高但可能包含旧数据,使用前需手动覆盖)
const buf5 = Buffer.allocUnsafe(1024);
buf5.fill(0); // 安全地清零4.2 Buffer 常用操作
const buf = Buffer.from('Node.js Buffer');
// 长度与字节
console.log(buf.length); // 15(字节数,非字符数)
console.log(buf.byteLength); // 15
// 读写数据
console.log(buf[0]); // 78(ASCII 'N')
console.log(buf.toString()); // 'Node.js Buffer'
console.log(buf.toString('hex')); // 十六进制表示
// 切片(返回新的 Buffer,共享内存)
const slice = buf.slice(0, 4); // 'Node'
slice[0] = 0x4e; // 修改会影响原 Buffer
// 复制到新 Buffer
const dest = Buffer.alloc(4);
buf.copy(dest, 0, 0, 4); // dest 为 'Node'
// 拼接
const bufA = Buffer.from('Hello ');
const bufB = Buffer.from('World');
const combined = Buffer.concat([bufA, bufB]);
console.log(combined.toString()); // 'Hello World'4.3 编码转换
Buffer 支持多种字符编码转换:
| 编码 | 说明 |
|---|---|
utf-8 / utf8 | 多字节编码的 Unicode 字符 |
base64 | Base64 编码 |
hex | 十六进制编码 |
ascii | 7 位 ASCII 编码 |
latin1 / binary | ISO-8859-1 编码 |
const buf = Buffer.from('中文测试', 'utf-8');
console.log(buf.toString('base64')); // 5Lit5paH5rWL6K+V
console.log(buf.toString('hex')); // e4b8ade69687e6b58be8af95
// 从其他编码还原
const original = Buffer.from('5Lit5paH5rWL6K+V', 'base64');
console.log(original.toString('utf-8')); // 中文测试4.4 Buffer 与 TypedArray
Buffer 继承自 Uint8Array,因此可以互操作:
const buf = Buffer.from([0, 1, 2, 3, 4]);
const uint8 = new Uint8Array(buf);
uint8[0] = 255; // 修改会影响原 Buffer(共享内存)
console.log(buf[0]); // 255
// 创建不共享内存的副本
const uint8Copy = new Uint8Array(buf);五、Cluster(集群)
Node.js 运行在单线程上,无法充分利用多核 CPU。Cluster 模块允许创建子进程(Worker),将负载分布到多个 CPU 核心上,从而提高应用的吞吐量和可用性。
5.1 多进程架构
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
// 主进程:管理子进程
console.log(`主进程 ${process.pid} 启动`);
// 根据 CPU 核心数创建子进程
const cpuCount = os.cpus().length;
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
// 监听子进程退出事件,自动重启
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 退出,重启中...`);
cluster.fork();
});
} else {
// 子进程:运行 HTTP 服务
http.createServer((req, res) => {
res.writeHead(200);
res.end(`来自工作进程 ${process.pid}`);
}).listen(8000);
}5.2 负载均衡策略
默认情况下,Node.js Cluster 使用**轮询(Round-Robin)**策略分发请求(Windows 除外,Windows 默认使用 shared socket 方式)。可以通过环境变量设置:
// 启用轮询调度(除 Windows 外的默认行为)
process.env.NODE_CLUSTER_SCHED_POLICY = 'rr';
// 使用操作系统调度(shared socket)
process.env.NODE_CLUSTER_SCHED_POLICY = 'none';| 策略 | 特点 | 适用场景 |
|---|---|---|
| Round-Robin | 主进程按顺序分发连接,负载均衡效果好 | 短连接场景,如 HTTP API |
| Shared Socket | 操作系统负责分发,各进程共享端口 | 长连接场景,如 WebSocket |
5.3 进程间通信(IPC)
主进程与子进程之间通过 IPC 通道进行通信:
if (cluster.isMaster) {
const worker = cluster.fork();
// 向子进程发消息
worker.send({ type: 'greeting', data: 'Hello from master' });
// 接收子进程消息
worker.on('message', (msg) => {
console.log(`收到子进程消息:`, msg);
});
} else {
// 接收主进程消息
process.on('message', (msg) => {
console.log(`收到主进程消息:`, msg);
// 回复主进程
process.send({ type: 'reply', data: 'Hello from worker' });
});
}5.4 共享状态
由于各子进程是独立的内存空间,无法直接共享状态。常见的解决方案:
- 共享数据库:使用 Redis、MySQL 等外部存储共享数据。
- 消息转发:通过主进程转发消息实现状态同步。
- 外部缓存:使用内存缓存如 Redis 或 Memcached。
六、process 全局对象
process 是 Node.js 的全局对象,提供当前 Node.js 进程的信息和控制能力。它不需要 require,在任何模块中都可以直接使用。
6.1 进程信息
// 进程 ID
console.log(process.pid); // 12345
// 命令行参数
console.log(process.argv); // ['node', 'app.js', '--port=3000']
// 环境变量
console.log(process.env.NODE_ENV); // 'production'
console.log(process.env.PATH);
// 平台信息
console.log(process.platform); // 'win32' / 'linux' / 'darwin'
console.log(process.arch); // 'x64' / 'arm64'
// Node.js 版本
console.log(process.version); // 'v20.11.0'
console.log(process.versions); // { node: '20.11.0', v8: '11.3.244.8', ... }
// 内存使用
console.log(process.memoryUsage());
// { rss: 30.5 MB, heapTotal: 6.3 MB, heapUsed: 4.2 MB, external: 1.1 MB }6.2 进程控制
// 优雅退出
process.exitCode = 0;
process.exit();
// 监听退出事件(清理资源)
process.on('exit', (code) => {
console.log(`进程退出,退出码: ${code}`);
// 注意:exit 事件中只能执行同步操作
});
// 捕获未处理异常
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
process.exit(1); // 建议退出,防止处于不一致状态
});
// 捕获未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});6.3 信号处理与标准流
// 信号处理
process.on('SIGINT', () => {
console.log('收到 SIGINT 信号(Ctrl+C)');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('收到 SIGTERM 信号,优雅关闭...');
server.close(() => process.exit(0));
});
// 标准 I/O
process.stdin.on('data', (data) => {
process.stdout.write(`输入: ${data}`);
});七、与传统浏览器环境对比
Node.js 和浏览器环境虽然都使用 JavaScript,但它们在设计目标、API 和运行时行为上有显著差异。
| 对比维度 | Node.js | 浏览器 |
|---|---|---|
| 运行目标 | 服务端应用、命令行工具 | 用户界面交互 |
| 全局对象 | global | window |
| 模块系统 | CommonJS / ES Module | ES Module(<script type="module">) |
| DOM/BOM | 无 | 完整的 DOM、BOM API |
| 文件系统 | fs 模块提供完整文件操作 | 受安全沙箱限制,无法直接访问文件系统 |
| 网络请求 | http / https / net 模块 | fetch() / XMLHttpRequest / WebSocket |
| 并发模型 | 单线程 + 事件循环 + Worker Threads | 单线程 + 事件循环 + Web Workers |
| 进程管理 | process / cluster / child_process | 无(仅 Service Worker) |
| 二进制数据 | Buffer(继承 Uint8Array) | ArrayBuffer / TypedArray / Blob |
| 定时器精度 | 毫秒级(受事件循环影响) | 毫秒级(有最小延迟 4ms) |
| 环境变量 | process.env | 无原生支持 |
| 入口 | module.exports / export | 无模块导出概念(通过全局变量暴露) |
7.1 全局 API 差异
Node.js 中 global 对象对应浏览器的 window,但两者暴露的 API 完全不同:
// Node.js 全局对象
console.log(global === globalThis); // true
// 浏览器全局对象
console.log(window === globalThis); // true
// 共同的全局对象(ES 标准)
console.log(globalThis); // 在不同环境中指向不同的全局对象7.2 事件循环差异
虽然两者都基于事件循环,但实现细节不同:
- 浏览器:事件循环由 HTML 规范定义,有多个任务源(Task Source),微任务在每次宏任务后清空。
- Node.js:事件循环由 libuv 实现,分为六个阶段,
process.nextTick是 Node.js 特有的机制。
7.3 适用场景对比
- 浏览器 JavaScript:专注于 UI 渲染、用户交互、DOM 操作、前端路由和状态管理。
- Node.js:专注于 I/O 密集型任务——Web 服务器、API 网关、构建工具、脚本自动化、实时通信。
实操 Demo
以下 Demo 展示了 Node.js Stream(流)的核心概念在前端中的模拟实现,包括可读流、转换流、可写流以及管道处理的可视化效果: