跳转至

第 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
2
3
4
5
请求1 → 线程1 ──阻塞等待数据库──→ 返回
请求2 → 线程2 ──阻塞等待数据库──→ 返回
请求3 → 线程3 ──阻塞等待数据库──→ 返回
...
请求10000 → 线程10000 → 💥 内存耗尽!

Node.js 模型:单线程 + 事件循环 + 非阻塞 I/O

1
2
3
4
5
请求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

对比阻塞版本:

1
2
3
4
// ❌ 阻塞版本:整个程序卡在这里等待
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 默认)

1
2
3
4
5
6
7
8
9
// 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
2
3
4
5
1. 解析模块路径(相对路径 / 内置模块 / node_modules)
2. 检查缓存(已加载的模块不会重新加载)
3. 读取文件内容
4. 包装函数:(function(exports, require, module, __filename, __dirname) { ... })
5. 执行函数,返回 module.exports

ES Modules(Node.js 14+ 稳定)

1
2
3
4
5
6
7
8
// 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?

1
2
3
4
5
6
7
8
// ❌ 问题:读取 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 设计规范