Node.js 基础
问题
Node.js 是什么?它有哪些特点和适用场景?
答案
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,用于在服务端运行 JavaScript 代码。
核心特点
1. 单线程 + 事件驱动
// Node.js 使用单线程处理请求
// 但通过事件循环实现高并发
import { createServer } from 'http';
const server = createServer((req, res) => {
// 每个请求都在同一个线程处理
// 但 I/O 操作是异步的,不会阻塞
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000);
console.log('Server running at http://localhost:3000/');
2. 非阻塞 I/O
import { readFile } from 'fs';
console.log('开始读取文件');
// 异步读取,不阻塞后续代码
readFile('large-file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('文件读取完成');
});
console.log('继续执行其他代码'); // 这行先执行
// 输出顺序:
// 开始读取文件
// 继续执行其他代码
// 文件读取完成
3. V8 引擎
- JIT 编译:将 JavaScript 编译为机器码执行
- 垃圾回收:自动内存管理
- 持续优化:热点代码优化
// V8 的隐藏类优化
// 好的写法:对象形状一致
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 避免:动态添加属性改变对象形状
const obj: Record<string, number> = {};
obj.a = 1; // 改变形状
obj.b = 2; // 再次改变形状
4. 跨平台
Node.js 可以运行在:
- Windows
- macOS
- Linux
- 其他 Unix 系统
架构组成
| 组件 | 作用 |
|---|---|
| V8 | JavaScript 引擎,编译执行 JS 代码 |
| libuv | 跨平台异步 I/O 库,实现事件循环 |
| Bindings | 连接 JavaScript 和 C++ 代码 |
| 核心模块 | fs、http、net、path 等内置模块 |
适用场景
适合
| 场景 | 说明 |
|---|---|
| I/O 密集型 | 文件操作、网络请求、数据库查询 |
| 实时应用 | 聊天、协作工具、在线游戏 |
| API 服务 | RESTful API、GraphQL 服务 |
| 微服务 | 轻量级服务、快速启动 |
| 工具开发 | CLI 工具、构建工具、脚手架 |
| SSR | React/Vue 服务端渲染 |
| BFF | Backend For Frontend 层 |
不适合
| 场景 | 原因 |
|---|---|
| CPU 密集型 | 复杂计算会阻塞事件循环 |
| 大量计算 | 图像处理、视频编码、机器学习 |
| 内存密集型 | V8 内存限制(默认约 1.4GB) |
CPU 密集型的解决方案
- 使用 Worker Threads 多线程
- 调用 C++ 插件
- 拆分为微服务,用其他语言处理
Node.js vs 浏览器
| 特性 | Node.js | 浏览器 |
|---|---|---|
| JavaScript 引擎 | V8 | V8/SpiderMonkey/JavaScriptCore |
| 全局对象 | global / globalThis | window / globalThis |
| DOM/BOM | ❌ 无 | ✅ 有 |
| 文件系统 | ✅ fs 模块 | ❌ 受限(File API) |
| 网络 | ✅ 完整(http/net) | ❌ 受限(fetch/XHR) |
| 模块系统 | CommonJS + ESM | ESM |
| 多线程 | Worker Threads | Web Workers |
// 全局对象差异
// Node.js
console.log(global === globalThis); // true
// 浏览器
console.log(window === globalThis); // true
// 通用写法
console.log(globalThis); // 两边都可用
核心模块
// 内置模块,无需安装
import fs from 'fs'; // 文件系统
import path from 'path'; // 路径处理
import http from 'http'; // HTTP 服务
import https from 'https'; // HTTPS 服务
import url from 'url'; // URL 解析
import os from 'os'; // 操作系统信息
import crypto from 'crypto'; // 加密
import events from 'events'; // 事件
import stream from 'stream'; // 流
import util from 'util'; // 工具函数
import child_process from 'child_process'; // 子进程
import cluster from 'cluster'; // 集群
import worker_threads from 'worker_threads'; // 工作线程
全局变量
// 全局对象
console.log(global); // 全局命名空间
console.log(globalThis); // ES2020 标准
// 模块相关
console.log(__dirname); // 当前模块目录的绝对路径
console.log(__filename); // 当前模块文件的绝对路径
console.log(module); // 当前模块对象
console.log(exports); // 模块导出对象
console.log(require); // 模块引入函数
// 进程相关
console.log(process.env); // 环境变量
console.log(process.argv); // 命令行参数
console.log(process.cwd()); // 当前工作目录
console.log(process.pid); // 进程 ID
console.log(process.platform); // 操作系统平台
console.log(process.version); // Node.js 版本
// 定时器
setTimeout(() => {}, 1000);
setInterval(() => {}, 1000);
setImmediate(() => {});
process.nextTick(() => {});
ESM 中的差异
在 ES 模块中,__dirname 和 __filename 不可用,需要这样获取:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
常见面试问题
Q1: Node.js 为什么是单线程的?有什么优缺点?
答案:
Node.js 采用单线程模型主要是为了简化编程模型,避免多线程的复杂性(死锁、竞态条件等)。
优点:
- 简单:无需处理线程同步、锁等问题
- 内存占用低:无需为每个连接创建线程
- 高并发:事件驱动 + 非阻塞 I/O 处理大量连接
- 适合 I/O 密集型:I/O 等待时可处理其他请求
缺点:
- CPU 密集型任务阻塞:长时间计算会阻塞事件循环
- 无法利用多核 CPU:需要 cluster 或 worker_threads
- 错误处理敏感:未捕获异常会导致进程崩溃
// CPU 密集型会阻塞
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// ❌ 阻塞事件循环
app.get('/fib', (req, res) => {
const result = fibonacci(45); // 阻塞!
res.send({ result });
});
// ✅ 使用 Worker Thread
import { Worker } from 'worker_threads';
app.get('/fib', (req, res) => {
const worker = new Worker('./fib-worker.js', {
workerData: { n: 45 }
});
worker.on('message', (result) => res.send({ result }));
});
Q2: Node.js 如何处理高并发?
答案:
Node.js 通过以下机制处理高并发:
- 事件循环:单线程轮询处理事件
- 非阻塞 I/O:I/O 操作异步执行,不阻塞主线程
- libuv 线程池:文件 I/O 等操作在后台线程执行
- 事件驱动:通过回调/Promise 处理异步结果
// 处理 1 万个并发连接
import { createServer } from 'http';
const server = createServer((req, res) => {
// 每个请求快速处理,不阻塞
res.writeHead(200);
res.end('OK');
});
server.listen(3000);
// 单线程可以轻松处理上万并发
// 因为大部分时间在等待 I/O,不是在计算
Q3: 什么是 libuv?它的作用是什么?
答案:
libuv 是 Node.js 的底层库,提供:
| 功能 | 说明 |
|---|---|
| 事件循环 | 实现非阻塞 I/O |
| 异步文件操作 | 文件读写、监控 |
| 异步网络 | TCP/UDP Socket |
| 线程池 | 处理阻塞操作 |
| 信号处理 | 进程信号 |
| 定时器 | setTimeout/setInterval |
| 跨平台抽象 | 统一 Windows/Unix API |
// libuv 线程池默认 4 个线程
// 可通过环境变量调整
process.env.UV_THREADPOOL_SIZE = '8';
// 以下操作使用线程池:
// - fs 文件操作(除了 fs.watch)
// - crypto 加密操作
// - dns.lookup
// - 某些压缩操作
Q4: Node.js 的 process.nextTick 和 setImmediate 有什么区别?
答案:
// process.nextTick:在当前操作完成后、事件循环继续前执行
// setImmediate:在事件循环的 check 阶段执行
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
console.log('同步代码');
// 输出顺序:
// 同步代码
// nextTick
// setImmediate
| 特性 | process.nextTick | setImmediate |
|---|---|---|
| 执行时机 | 当前阶段结束后立即执行 | check 阶段执行 |
| 队列 | 独立的 nextTick 队列 | 事件循环队列 |
| 优先级 | 更高 | 较低 |
| 递归风险 | 可能阻塞 I/O | 不会阻塞 |
注意
递归调用 process.nextTick 会阻塞事件循环,应优先使用 setImmediate。
Q5: 如何在 Node.js 中处理 CPU 密集型任务?
答案:
// 方案一:Worker Threads(推荐)
import { Worker, isMainThread, workerData, parentPort } from 'worker_threads';
if (isMainThread) {
const worker = new Worker(__filename, {
workerData: { n: 40 }
});
worker.on('message', (result) => {
console.log('Result:', result);
});
} else {
const { n } = workerData;
const result = fibonacci(n);
parentPort?.postMessage(result);
}
// 方案二:Child Process
import { fork } from 'child_process';
const child = fork('./heavy-computation.js');
child.send({ data: 'input' });
child.on('message', (result) => {
console.log('Result:', result);
});
// 方案三:Cluster 多进程
import cluster from 'cluster';
import { cpus } from 'os';
if (cluster.isPrimary) {
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
} else {
// 工作进程
}
// 方案四:调用 C++ 插件(N-API/nan)
// 对于性能关键的计算,使用原生模块