第 1 章:Node.js 原理详解
Node.js 是现代 Web 开发的基石。理解它的运行原理,是理解 Express、FastAPI 等框架的前提。
1.1 什么是 Node.js?
Node.js 是一个 基于 V8 引擎的 JavaScript 运行时环境,由 Ryan Dahl 于 2009 年创建。
它让 JavaScript 不再局限于浏览器,可以在服务器端运行。
核心组成
| ┌─────────────────────────────────────────┐
│ Node.js │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ V8 引擎 │ │ libuv (C++) │ │
│ │ (JS→机器码) │ │ (异步 I/O 层) │ │
│ └─────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 内置模块 (fs, http, path...) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
|
- V8 引擎:Google 开源的高性能 JS 引擎,将 JavaScript 编译为机器码直接执行
- libuv:跨平台的异步 I/O 库,实现了 Event Loop 和线程池
- 内置模块:Node.js 提供的标准库(文件系统、HTTP、路径等)
1.2 Event Loop(事件循环)—— Node.js 的心脏
为什么需要 Event Loop?
传统服务器模型(如 Apache):每个请求一个线程。
| 请求1 → 线程1 ──阻塞等待数据库──→ 返回
请求2 → 线程2 ──阻塞等待数据库──→ 返回
请求3 → 线程3 ──阻塞等待数据库──→ 返回
...
请求10000 → 线程10000 → 💥 内存耗尽!
|
Node.js 模型:单线程 + 事件循环 + 非阻塞 I/O。
| 请求1 → 发起数据库查询 → 注册回调 → 立即返回处理下一个
请求2 → 发起数据库查询 → 注册回调 → 立即返回处理下一个
请求3 → 发起数据库查询 → 注册回调 → 立即返回处理下一个
...
数据库完成 → 回调被 Event Loop 调度执行 → 返回结果
|
Event Loop 的 6 个阶段
| ┌───────────────────────────┐
┌─→│ timers │ setTimeout / setInterval 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 系统操作回调(如 TCP 错误)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ I/O 回调,最重要的阶段
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │ socket.on('close') 等
│ └───────────────────────────┘
|
代码示例:理解 Event Loop
| // 示例 1:理解执行顺序
console.log('1. 同步代码开始');
setTimeout(() => {
console.log('4. setTimeout 回调 (timers 阶段)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise 回调 (microtask)');
});
console.log('2. 同步代码结束');
// 输出:
// 1. 同步代码开始
// 2. 同步代码结束
// 3. Promise 回调 (microtask)
// 4. setTimeout 回调 (timers 阶段)
|
关键概念:Microtask Queue(微任务队列)
Event Loop 在每个阶段结束后,会先清空 microtask 队列(Promise.then、process.nextTick),再进入下一个阶段。
| 优先级:process.nextTick > Promise.then > setTimeout
|
示例 2:非阻塞 I/O 的实际效果
| const fs = require('fs');
console.log('开始读取文件');
// 非阻塞读取:不会等待文件读取完成,而是注册回调后继续执行
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
console.log('文件读取完成,长度:', data.length);
});
console.log('文件读取已发起,继续执行其他任务');
// 输出:
// 开始读取文件
// 文件读取已发起,继续执行其他任务
// 文件读取完成,长度: 2048
|
对比阻塞版本:
| // ❌ 阻塞版本:整个程序卡在这里等待
const data = fs.readFileSync('/etc/passwd', 'utf8');
console.log('文件读取完成'); // 必须等文件读完才执行
console.log('这行要等上面执行完');
|
1.3 非阻塞 I/O 与线程池
libuv 的线程池
Node.js 主线程是单线程的,但 libuv 内部有一个 线程池(默认 4 个线程),用于处理某些阻塞操作:
| 操作类型 | 是否使用线程池 | 说明 |
| 网络 I/O(HTTP、TCP) | ❌ | 使用操作系统的 epoll/kqueue |
| 文件读取/写入 | ✅ | 使用线程池 |
| DNS 解析 | ✅ | 使用线程池 |
| 加密操作(crypto) | ✅ | 使用线程池 |
| CPU 密集型计算 | ❌ | 需要 Worker Threads |
| // 调整线程池大小(默认 4)
process.env.UV_THREADPOOL_SIZE = 8;
|
线程池的坑
| // ❌ 问题:大量 CPU 密集型任务会阻塞线程池
const crypto = require('crypto');
for (let i = 0; i < 100; i++) {
// 这个操作使用线程池,4 个线程会排队
crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', (err, hash) => {
console.log('密码哈希完成');
});
}
// 此时,文件 I/O 也会被阻塞!因为它们共享同一个线程池
fs.readFile('large-file.txt', (err, data) => {
// 可能要等很久...
});
|
解决方案:Worker Threads
| const { Worker } = require('worker_threads');
// 主线程
const worker = new Worker('./cpu-intensive.js');
worker.on('message', (result) => {
console.log('计算完成:', result);
});
worker.postMessage({ data: [1, 2, 3, 4, 5] });
// cpu-intensive.js(Worker 线程)
const { parentPort } = require('worker_threads');
parentPort.on('message', ({ data }) => {
// CPU 密集型计算在这里执行,不阻塞主线程
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
parentPort.postMessage(sum);
});
|
1.4 模块系统
CommonJS(Node.js 默认)
| // math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const { add } = require('./math');
console.log(add(2, 3)); // 5
|
模块加载原理:
| 1. 解析模块路径(相对路径 / 内置模块 / node_modules)
2. 检查缓存(已加载的模块不会重新加载)
3. 读取文件内容
4. 包装函数:(function(exports, require, module, __filename, __dirname) { ... })
5. 执行函数,返回 module.exports
|
ES Modules(Node.js 14+ 稳定)
| // math.mjs
export function add(a, b) {
return a + b;
}
// app.mjs
import { add } from './math.mjs';
console.log(add(2, 3));
|
1.5 Stream(流)—— 处理大数据的核心
为什么需要 Stream?
| // ❌ 问题:读取 1GB 文件,整个加载到内存
const fs = require('fs');
const data = fs.readFileSync('1gb-video.mp4'); // 💥 内存爆炸
// ✅ 方案:Stream 逐块处理,内存占用恒定
const readStream = fs.createReadStream('1gb-video.mp4');
const writeStream = fs.createWriteStream('copy.mp4');
readStream.pipe(writeStream); // 内存占用 < 1MB
|
Stream 的四种类型
| 类型 | 说明 | 示例 |
| Readable | 可读流 | fs.createReadStream() |
| Writable | 可写流 | fs.createWriteStream() |
| Duplex | 可读可写 | net.Socket |
| Transform | 转换流 | zlib.createGzip() |
实际案例:大文件压缩上传
| const fs = require('fs');
const zlib = require('zlib');
const http = require('http');
// 读取大文件 → 压缩 → 上传,全程流式处理
fs.createReadStream('large-dataset.csv')
.pipe(zlib.createGzip())
.pipe(http.request({
hostname: 'api.example.com',
path: '/upload',
method: 'POST',
headers: { 'Content-Encoding': 'gzip' }
}));
// 内存占用:仅一个 chunk 大小(默认 64KB)
|
1.6 实际案例:用原生 Node.js 搭建 HTTP 服务器
| const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const { pathname, query } = parsedUrl;
// 路由
if (pathname === '/api/users' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]));
} else if (pathname === '/api/users' && req.method === 'POST') {
let body = '';
req.on('data', chunk => { body += chunk; }); // 流式读取
req.on('end', () => {
const user = JSON.parse(body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 3, ...user }));
});
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
|
这段代码展示了: - HTTP 服务器的基本结构 - 路由分发 - 流式读取请求体 - JSON 响应
Express 本质上就是对这段代码的封装和增强。
1.7 总结
| 概念 | 一句话 |
| V8 引擎 | 将 JS 编译为机器码,极速执行 |
| Event Loop | 单线程异步的核心机制,循环调度回调 |
| 非阻塞 I/O | 发起 I/O 后不等待,继续处理其他任务 |
| libuv 线程池 | 处理文件 I/O、DNS、加密等阻塞操作 |
| Stream | 逐块处理数据,内存占用恒定 |
| 模块系统 | CommonJS(require)和 ES Modules(import) |
Node.js 适合的场景: - I/O 密集型应用(API 服务、代理服务器、实时聊天) - 高并发、轻量级计算 - 前后端统一语言(JavaScript/TypeScript)
Node.js 不适合的场景: - CPU 密集型计算(图像处理、视频编码、机器学习训练) - 需要大量同步阻塞操作的场景
下一章:REST API 设计规范