Skip to content

Node.js 核心模块

Node.js 架构概览

整体架构

Node.js 的运行时架构由三层组成:JavaScript 应用层、Node.js Bindings(C++ 桥接层)、底层引擎层(V8 + libuv + 其他 C/C++ 库)。理解这一架构是掌握 Node.js 行为的根基。

┌─────────────────────────────────────────────────────────────────┐
│                     Node.js 运行时架构                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                  JavaScript 应用代码                        │  │
│  │              (你写的 js/ts 代码、npm 包)                    │  │
│  └───────────────────────────┬───────────────────────────────┘  │
│                              │                                  │
│  ┌───────────────────────────┴───────────────────────────────┐  │
│  │              Node.js Core Modules (JS 层)                  │  │
│  │         fs / http / stream / path / events ...             │  │
│  └───────────────────────────┬───────────────────────────────┘  │
│                              │                                  │
│  ┌───────────────────────────┴───────────────────────────────┐  │
│  │              Node.js Bindings (C++ 桥接层)                 │  │
│  │          将 JS 调用转换为底层 C/C++ 操作                    │  │
│  └──────┬────────────────────┬───────────────────┬───────────┘  │
│         │                    │                   │              │
│  ┌──────┴──────┐  ┌─────────┴─────────┐  ┌──────┴──────────┐  │
│  │    V8 引擎   │  │      libuv        │  │  其他 C/C++ 库   │  │
│  │             │  │                   │  │                 │  │
│  │ JS 解析执行  │  │ 事件循环           │  │ c-ares (DNS)    │  │
│  │ JIT 编译    │  │ 异步 I/O          │  │ OpenSSL (加密)   │  │
│  │ 内存管理/GC │  │ 线程池            │  │ zlib (压缩)      │  │
│  │ 调用栈      │  │ 跨平台抽象        │  │ llhttp (解析)    │  │
│  └─────────────┘  └───────────────────┘  └─────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    操作系统 (Linux/macOS/Windows)           │  │
│  │           epoll / kqueue / IOCP / 文件系统 / 网络           │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

V8 引擎

V8 是 Google 开发的高性能 JavaScript 引擎,也是 Chrome 浏览器的 JS 引擎。在 Node.js 中,V8 负责将 JavaScript 源码解析、编译为机器码并执行。

V8 的关键机制:

  • JIT 编译:V8 不是纯解释执行,而是将热点代码(频繁执行的代码)通过 TurboFan 优化编译器编译为高度优化的机器码
  • 隐藏类(Hidden Class):V8 为每个对象动态创建隐藏类以实现快速属性访问,这就是为什么始终以相同顺序初始化对象属性能带来性能提升
  • 内存管理:V8 使用分代垃圾回收,新生代采用 Scavenge 算法,老生代采用 Mark-Sweep + Mark-Compact 算法
  • 堆内存限制:64 位系统下 V8 堆内存默认上限约 1.7GB,可通过 --max-old-space-size 调整

libuv 与事件循环

libuv 是一个跨平台异步 I/O 库,它是 Node.js 实现非阻塞 I/O 的核心。libuv 封装了不同操作系统的异步机制:Linux 上用 epoll,macOS 上用 kqueue,Windows 上用 IOCP。

┌───────────────────────────────────────────────────────┐
│                    libuv 事件循环                       │
│                                                       │
│   ┌──────────────────────────────────────────────┐    │
│   │          ┌─────────────────┐                 │    │
│   │    ┌────→│     timers      │ setTimeout/     │    │
│   │    │     │                 │ setInterval     │    │
│   │    │     └────────┬────────┘                 │    │
│   │    │              │                          │    │
│   │    │     ┌────────┴────────┐                 │    │
│   │    │     │ pending callbacks│ 系统级回调      │    │
│   │    │     └────────┬────────┘                 │    │
│   │    │              │                          │    │
│   │    │     ┌────────┴────────┐                 │    │
│   │    │     │   idle/prepare  │ 内部使用         │    │
│   │    │     └────────┬────────┘                 │    │
│   │    │              │                          │    │
│   │    │     ┌────────┴────────┐                 │    │
│   │    │     │      poll       │ I/O 回调        │    │
│   │    │     │                 │ (文件/网络)      │    │
│   │    │     └────────┬────────┘                 │    │
│   │    │              │                          │    │
│   │    │     ┌────────┴────────┐                 │    │
│   │    │     │     check      │ setImmediate    │    │
│   │    │     └────────┬────────┘                 │    │
│   │    │              │                          │    │
│   │    │     ┌────────┴────────┐                 │    │
│   │    └─────│ close callbacks │ socket.close()  │    │
│   │          └─────────────────┘                 │    │
│   └──────────────────────────────────────────────┘    │
│                                                       │
│   每个阶段之间都会执行:                                  │
│   1. process.nextTick 队列 (微任务,最高优先级)          │
│   2. Promise 微任务队列                                 │
│                                                       │
│   libuv 线程池 (默认 4 个线程,UV_THREADPOOL_SIZE):      │
│   ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐        │
│   │Thread 1│ │Thread 2│ │Thread 3│ │Thread 4│        │
│   │ DNS    │ │ fs     │ │ crypto │ │ zlib   │        │
│   └────────┘ └────────┘ └────────┘ └────────┘        │
└───────────────────────────────────────────────────────┘

单线程模型

Node.js 的"单线程"指的是 JavaScript 代码的执行在单一线程(主线程)上运行。但 Node.js 本身并非只有一个线程:

  • 主线程:执行 JS 代码、事件循环
  • libuv 线程池:处理文件 I/O、DNS 查询、部分加密操作(默认 4 个线程)
  • V8 辅助线程:垃圾回收、JIT 编译等
js
const crypto = require('crypto')

const start = Date.now()

for (let i = 0; i < 4; i++) {
  crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', () => {
    console.log(`任务 ${i} 完成,耗时: ${Date.now() - start}ms`)
  })
}

在 4 核 CPU 上运行此代码,4 个加密任务几乎同时完成(约 80ms),因为 libuv 线程池默认有 4 个线程并行处理。如果增加到 5 个任务,第 5 个任务的耗时将接近前 4 个的两倍,因为它需要等待线程池中有空闲线程。

js
process.env.UV_THREADPOOL_SIZE = 8

const crypto = require('crypto')

const start = Date.now()

for (let i = 0; i < 8; i++) {
  crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', () => {
    console.log(`任务 ${i} 完成,耗时: ${Date.now() - start}ms`)
  })
}

将线程池大小调整为 8 后,8 个任务可以并行完成。UV_THREADPOOL_SIZE 最大值为 1024。


fs 模块

概述

fs(File System)模块是 Node.js 与文件系统交互的核心模块,提供了三套风格的 API:同步(Sync)、回调异步(Callback)和 Promise 异步。理解它们的差异是掌握 Node.js I/O 模型的基础。

┌─────────────────────────────────────────────────────────────┐
│                     fs 模块 API 体系                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │  同步 API    │  │  回调异步 API     │  │  Promise API  │  │
│  │             │  │                  │  │               │  │
│  │ readFileSync│  │ readFile(cb)     │  │ fs/promises   │  │
│  │ 阻塞主线程  │  │ 回调地狱风险      │  │ async/await   │  │
│  │ 适合启动阶段 │  │ 高性能场景        │  │ 现代推荐写法   │  │
│  └─────────────┘  └──────────────────┘  └───────────────┘  │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              流式 API(Stream)                       │    │
│  │  createReadStream / createWriteStream               │    │
│  │  处理大文件、内存友好、支持背压                         │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

三套 API 风格对比

特性同步 API回调异步 APIPromise API
引入方式require('fs')require('fs')require('fs/promises')
是否阻塞阻塞主线程不阻塞不阻塞
错误处理try/catch回调第一个参数try/catch + async/await
适用场景启动配置读取高并发 I/O现代异步流程
函数后缀*Sync

readFile 与 writeFile

三种方式读取文件:

js
const fs = require('fs')
const fsp = require('fs/promises')

const data1 = fs.readFileSync('./config.json', 'utf-8')

fs.readFile('./config.json', 'utf-8', (err, data) => {
  if (err) throw err
  console.log(data)
})

async function readConfig() {
  const data = await fsp.readFile('./config.json', 'utf-8')
  return JSON.parse(data)
}

三种方式写入文件:

js
const fs = require('fs')
const fsp = require('fs/promises')

fs.writeFileSync('./output.txt', 'Hello Node.js', 'utf-8')

fs.writeFile('./output.txt', 'Hello Node.js', 'utf-8', (err) => {
  if (err) throw err
})

async function writeData() {
  await fsp.writeFile('./output.txt', 'Hello Node.js', 'utf-8')
}

目录操作

js
const fsp = require('fs/promises')
const path = require('path')

async function ensureDir(dirPath) {
  await fsp.mkdir(dirPath, { recursive: true })
}

async function listFiles(dirPath) {
  const entries = await fsp.readdir(dirPath, { withFileTypes: true })
  const result = { files: [], dirs: [] }
  for (const entry of entries) {
    if (entry.isFile()) {
      result.files.push(entry.name)
    } else if (entry.isDirectory()) {
      result.dirs.push(entry.name)
    }
  }
  return result
}

async function walkDir(dir) {
  const files = []
  const entries = await fsp.readdir(dir, { withFileTypes: true })
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)
    if (entry.isDirectory()) {
      files.push(...await walkDir(fullPath))
    } else {
      files.push(fullPath)
    }
  }
  return files
}

async function getFileInfo(filePath) {
  const stats = await fsp.stat(filePath)
  return {
    size: stats.size,
    isFile: stats.isFile(),
    isDirectory: stats.isDirectory(),
    created: stats.birthtime,
    modified: stats.mtime,
    permissions: stats.mode.toString(8)
  }
}

文件监听 fs.watch

fs.watch 利用操作系统原生的文件变化通知机制来监听文件或目录的变化:

js
const fs = require('fs')

const watcher = fs.watch('./src', { recursive: true }, (eventType, filename) => {
  console.log(`事件类型: ${eventType}`)
  console.log(`变化文件: ${filename}`)
})

watcher.on('error', (err) => {
  console.error('监听错误:', err)
})

process.on('SIGINT', () => {
  watcher.close()
  process.exit(0)
})

fs.watchfs.watchFile 对比:

特性fs.watchfs.watchFile
底层实现操作系统原生通知(inotify/kqueue/FSEvents)轮询(stat polling)
性能高效,O(1) 通知消耗 CPU,定时 stat 调用
跨平台一致性行为略有差异一致但慢
递归监听macOS/Windows 支持不支持
适用场景开发工具、热更新网络文件系统(NFS)

fs.watch vs chokidar

fs.watch 是 Node.js 原生 API,但在实际项目中往往使用 chokidar 这个第三方库。两者的核心差异:

┌─────────────────────────────────────────────────────────┐
│           fs.watch 的已知问题                              │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. macOS 上文件名报告不准确                               │
│  2. 某些编辑器(Vim/Sublime)保存文件时触发两次事件           │
│  3. Linux 上 recursive 选项不可用(Node < 19)            │
│  4. 不报告文件名(某些平台)                               │
│  5. rename 事件语义在不同平台不一致                         │
│                                                         │
│  chokidar 在底层使用 fs.watch + fs.watchFile 的组合       │
│  来解决这些跨平台问题,并提供更完善的 API                    │
└─────────────────────────────────────────────────────────┘
特性fs.watchchokidar
依赖内置模块,零依赖第三方包
跨平台一致性行为差异大封装了平台差异
重复事件去抖内置
初始扫描事件支持 ready 事件
glob 模式不支持支持
忽略规则不支持支持 ignored 选项
稳定性原始生产级

chokidar 的典型用法(Vite、Webpack 等构建工具的底层都在使用它):

js
const chokidar = require('chokidar')

const watcher = chokidar.watch('./src', {
  ignored: /(^|[\/\\])\../,
  persistent: true,
  ignoreInitial: true,
  awaitWriteFinish: {
    stabilityThreshold: 100,
    pollInterval: 50
  }
})

watcher
  .on('add', (filePath) => console.log(`新增: ${filePath}`))
  .on('change', (filePath) => console.log(`修改: ${filePath}`))
  .on('unlink', (filePath) => console.log(`删除: ${filePath}`))
  .on('ready', () => console.log('初始扫描完成'))
  .on('error', (err) => console.error('错误:', err))

awaitWriteFinish 选项是 chokidar 的精髓之一——它会等待文件写入完成后再触发事件,避免了编辑器写入过程中的中间态触发。stabilityThreshold 表示文件大小保持不变多少毫秒后才认为写入完成。

大文件流式处理

当文件体积较大时,一次性读取整个文件会消耗大量内存。流式 API 可以分块读取和写入,内存占用恒定。

┌──────────┐    chunk     ┌──────────┐    chunk     ┌──────────┐
│          │ ──────────→  │          │ ──────────→  │          │
│ 磁盘文件  │    64KB      │  内存缓冲  │    64KB      │ 目标文件  │
│          │ ──────────→  │  区(Buffer)│ ──────────→  │          │
└──────────┘              └──────────┘              └──────────┘
  ReadStream                                        WriteStream

使用流复制大文件:

js
const fs = require('fs')
const { pipeline } = require('stream/promises')

async function copyFile(src, dest) {
  await pipeline(
    fs.createReadStream(src, { highWaterMark: 64 * 1024 }),
    fs.createWriteStream(dest)
  )
}

逐行读取大型日志文件:

js
const fs = require('fs')
const readline = require('readline')

async function processLogFile(filePath) {
  const rl = readline.createInterface({
    input: fs.createReadStream(filePath),
    crlfDelay: Infinity
  })

  let lineCount = 0
  let errorCount = 0

  for await (const line of rl) {
    lineCount++
    if (line.includes('ERROR')) {
      errorCount++
    }
  }

  return { lineCount, errorCount }
}

path 模块

概述

path 模块用于处理文件路径和目录路径。不同操作系统的路径分隔符不同(Windows 用 \,POSIX 用 /),path 模块会自动处理这些差异,保证代码的跨平台兼容性。

┌─────────────────────────────────────────────┐
│              path 路径组成                    │
│                                             │
│  /home/user/projects/app/src/index.js       │
│  ├─────┤                                    │
│   root                                      │
│  ├─────────────────────────┤                │
│           dir                               │
│                              ├──────────┤   │
│                                 base        │
│                              ├────┤         │
│                               name          │
│                                   ├────┤    │
│                                    ext      │
└─────────────────────────────────────────────┘

path.join vs path.resolve 区别

这是面试中高频考点。两者都能拼接路径,但行为完全不同:

js
const path = require('path')

path.join('a', 'b', 'c')
path.resolve('a', 'b', 'c')

path.join('/a', 'b', 'c')
path.resolve('/a', 'b', 'c')

path.join('a', '/b', 'c')
path.resolve('a', '/b', 'c')
方法行为特点返回值特点
path.join纯粹拼接路径片段,处理 ...,不关心绝对/相对可能返回相对路径
path.resolve从右向左处理,遇到绝对路径即停止,未遇到则以 cwd 为基准总是返回绝对路径

path.resolve 的本质是模拟在终端中 cd 到指定路径后执行 pwd 的结果:

js
const path = require('path')

path.resolve('/foo', 'bar', 'baz')
path.resolve('/foo', '/bar', 'baz')

path.resolve('foo', 'bar')

path.resolve('foo', 'bar') 等价于先 cd foo,再 cd bar,最终得到 当前工作目录/foo/barpath.resolve('/foo', '/bar', 'baz') 处理到 /bar 时遇到绝对路径,之前的 /foo 被忽略,最终返回 /bar/baz

path.parse 与 path.format

js
const path = require('path')

const parsed = path.parse('/home/user/docs/letter.txt')

parsed 的结构为:

{
  root: '/',
  dir: '/home/user/docs',
  base: 'letter.txt',
  name: 'letter',
  ext: '.txt'
}
js
const path = require('path')

const formatted = path.format({
  root: '/',
  dir: '/home/user/docs',
  base: 'letter.txt'
})

path.basename('/foo/bar/baz.js')
path.basename('/foo/bar/baz.js', '.js')
path.extname('index.html')
path.extname('index.coffee.md')
path.extname('index.')
path.extname('.hidden')
path.dirname('/foo/bar/baz.js')

path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')

path.normalize('/foo/bar//baz/asdf/quux/..')

跨平台路径处理(posix vs win32)

path 模块会根据当前操作系统自动使用对应的实现。但你可以通过 path.posixpath.win32 显式指定平台:

js
const path = require('path')

console.log(path.sep)
console.log(path.delimiter)

path.win32.join('C:\\Users', 'docs', 'file.txt')
path.posix.join('/home/user', 'docs', 'file.txt')

path.win32.parse('C:\\Users\\docs\\file.txt')
path.posix.parse('/home/user/docs/file.txt')

实际开发中的跨平台最佳实践:

js
const path = require('path')

function toUnixPath(p) {
  return p.split(path.sep).join('/')
}

function resolveFromRoot(...segments) {
  return path.resolve(__dirname, '..', ...segments)
}

在构建工具中(如 Webpack、Vite),URL 路径始终使用 / 分隔符,因此需要用 path.posix 或手动转换。而文件系统操作则应始终使用 path.join / path.resolve,让 Node.js 自动处理平台差异。


stream 模块

四种流类型

Node.js 中的流(Stream)是处理数据的抽象接口,所有流都是 EventEmitter 的实例。

┌─────────────────────────────────────────────────────────────────┐
│                     Node.js 四种流类型                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Readable(可读流)          Writable(可写流)                   │
│  ┌──────────────┐           ┌──────────────┐                    │
│  │  数据源       │ ────→    │  数据目标     │                    │
│  │ fs.createReadStream      │ fs.createWriteStream              │
│  │ http req (客户端收到)     │ http res (服务端发送)              │
│  │ process.stdin            │ process.stdout                    │
│  └──────────────┘           └──────────────┘                    │
│                                                                 │
│  Duplex(双工流)             Transform(转换流)                 │
│  ┌──────────────┐           ┌──────────────┐                    │
│  │ 读 ←────     │           │ 读 ←────     │                    │
│  │              │           │  ↑ 转换      │                    │
│  │ 写 ────→     │           │ 写 ────→     │                    │
│  │ net.Socket   │           │ zlib.createGzip                   │
│  │ TCP 连接      │           │ crypto.createCipher               │
│  └──────────────┘           └──────────────┘                    │
│                                                                 │
│  Duplex: 读写独立,两侧无关联                                    │
│  Transform: 写入数据经过转换后从读取侧输出                        │
└─────────────────────────────────────────────────────────────────┘
流类型作用典型实例核心事件
Readable数据的来源fs.createReadStreamhttp.IncomingMessagedataenderror
Writable数据的目标fs.createWriteStreamhttp.ServerResponsedrainfinisherror
Duplex同时可读可写net.Socket、TCP 连接兼具两者事件
Transform读写之间有转换zlib.createGzipcryptodataenderror

流无处不在

Node.js 中流不是一个孤立的模块,它贯穿了几乎所有核心模块。理解这一点是掌握 Node.js 架构的关键。

┌─────────────────────────────────────────────────────────────┐
│               Node.js 中流的分布                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  模块              Readable              Writable           │
│  ─────────────────────────────────────────────────────────  │
│  fs               createReadStream      createWriteStream   │
│  http             IncomingMessage(req)  ServerResponse(res)  │
│  net              Socket (读取侧)        Socket (写入侧)     │
│  zlib             createGunzip          createGzip           │
│  crypto           createDecipher        createCipher         │
│  process          process.stdin         process.stdout       │
│  child_process    child.stdout          child.stdin          │
│  readline         Interface             -                    │
│                                                             │
│  所有这些流都继承自 EventEmitter                               │
│  所有这些流都支持 pipe / pipeline 互联                        │
└─────────────────────────────────────────────────────────────┘

这意味着你可以将 HTTP 请求体直接 pipe 到文件、将文件 pipe 到 gzip 压缩再 pipe 到 HTTP 响应、将子进程的输出 pipe 到父进程的日志——一切都是流的组合。

自定义可读流和可写流:

js
const { Readable, Writable } = require('stream')

class CounterStream extends Readable {
  constructor(max) {
    super()
    this.max = max
    this.current = 0
  }

  _read() {
    if (this.current < this.max) {
      this.current++
      this.push(`${this.current}\n`)
    } else {
      this.push(null)
    }
  }
}

class UpperCaseWriter extends Writable {
  _write(chunk, encoding, callback) {
    process.stdout.write(chunk.toString().toUpperCase())
    callback()
  }
}

const counter = new CounterStream(5)
const writer = new UpperCaseWriter()
counter.pipe(writer)

背压(Backpressure)机制

当可写流处理数据的速度跟不上可读流产生数据的速度时,就会产生背压。如果不处理背压,数据会堆积在内存中,最终导致内存溢出。

正常情况(生产速度 = 消费速度):

  Readable ──chunk──→ ──chunk──→ ──chunk──→ Writable
            均匀流动                          及时消费

背压情况(生产速度 > 消费速度):

  Readable ──chunk──→ chunk chunk chunk ←── Writable
            持续生产   ↑ 堆积在缓冲区中        处理较慢

背压处理流程:

  ┌──────────┐   write()    ┌──────────┐
  │ Readable │ ──────────→  │ Writable │
  │          │              │          │
  │          │  返回 false   │ 缓冲区满  │
  │  pause() │ ←──────────  │          │
  │          │              │          │
  │          │  'drain'事件  │ 缓冲区空  │
  │ resume() │ ←──────────  │          │
  └──────────┘              └──────────┘

背压的触发条件与 highWaterMark 直接相关。highWaterMark 是流内部缓冲区的水位线(默认 16KB),当缓冲区中的数据量达到或超过这个值时,writable.write() 返回 false,表示消费者已经"装不下了"。

手动处理背压:

js
const fs = require('fs')

const readable = fs.createReadStream('./huge-file.dat')
const writable = fs.createWriteStream('./output.dat')

readable.on('data', (chunk) => {
  const canWrite = writable.write(chunk)
  if (!canWrite) {
    readable.pause()
  }
})

writable.on('drain', () => {
  readable.resume()
})

readable.on('end', () => {
  writable.end()
})

pipe() 内部自动处理背压,但它的错误处理不够完善——如果链路中某个流出错,其他流不会自动销毁,可能导致内存泄漏。这正是 pipeline 诞生的原因。

pipeline vs pipe

stream.pipeline 是 Node.js 10+ 推荐的流管道 API,它自动处理背压和错误传播,并在管道完成或出错时进行清理。

js
const { pipeline } = require('stream/promises')
const fs = require('fs')
const zlib = require('zlib')

async function compress(input, output) {
  await pipeline(
    fs.createReadStream(input),
    zlib.createGzip(),
    fs.createWriteStream(output)
  )
}
pipeline 内部工作流程:

  ┌──────┐    pipe    ┌───────────┐    pipe    ┌──────┐
  │ Read │ ────────→  │ Transform │ ────────→  │Write │
  │Stream│            │  (Gzip)   │            │Stream│
  └──┬───┘            └─────┬─────┘            └──┬───┘
     │                      │                     │
     │    自动背压处理        │   自动背压处理        │
     │    错误自动传播        │   错误自动传播        │
     └──────────────────────┴─────────────────────┘
                    统一的 Promise / callback
                    自动清理所有流(destroy)

pipeline 相比 pipe 的优势:

特性pipepipeline
错误传播不传播,需手动监听每个流自动传播到最终回调
流清理出错后其他流不自动销毁自动 destroy 所有流
背压自动处理自动处理
Promise 支持stream/promises 提供
AbortSignal支持取消

使用 AbortController 取消流管道:

js
const { pipeline } = require('stream/promises')
const fs = require('fs')

async function downloadWithTimeout(src, dest, timeoutMs) {
  const ac = new AbortController()
  const timer = setTimeout(() => ac.abort(), timeoutMs)

  try {
    await pipeline(
      fs.createReadStream(src),
      fs.createWriteStream(dest),
      { signal: ac.signal }
    )
  } finally {
    clearTimeout(timer)
  }
}

手写 Transform 流实战

Transform 流是一种特殊的 Duplex 流,它接收输入数据进行转换后输出。实际应用场景包括数据加密、压缩、格式转换、日志过滤等。

CSV 转 JSON 的 Transform 流:

js
const { Transform, pipeline } = require('stream')
const fs = require('fs')

class CsvToJson extends Transform {
  constructor() {
    super({ readableObjectMode: true })
    this.headers = null
    this.buffer = ''
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString()
    const lines = this.buffer.split('\n')
    this.buffer = lines.pop()

    for (const line of lines) {
      if (!line.trim()) continue
      const values = line.split(',').map(v => v.trim())

      if (!this.headers) {
        this.headers = values
        continue
      }

      const obj = {}
      this.headers.forEach((header, i) => {
        obj[header] = values[i] || ''
      })
      this.push(JSON.stringify(obj) + '\n')
    }
    callback()
  }

  _flush(callback) {
    if (this.buffer.trim() && this.headers) {
      const values = this.buffer.split(',').map(v => v.trim())
      const obj = {}
      this.headers.forEach((header, i) => {
        obj[header] = values[i] || ''
      })
      this.push(JSON.stringify(obj) + '\n')
    }
    callback()
  }
}

pipeline(
  fs.createReadStream('./data.csv'),
  new CsvToJson(),
  fs.createWriteStream('./data.ndjson'),
  (err) => {
    if (err) console.error(err)
  }
)

数据过滤 Transform 流:

js
const { Transform } = require('stream')

class FilterStream extends Transform {
  constructor(predicate) {
    super({ objectMode: true })
    this.predicate = predicate
  }

  _transform(chunk, encoding, callback) {
    if (this.predicate(chunk)) {
      this.push(chunk)
    }
    callback()
  }
}

class MapStream extends Transform {
  constructor(mapper) {
    super({ objectMode: true })
    this.mapper = mapper
  }

  _transform(chunk, encoding, callback) {
    this.push(this.mapper(chunk))
    callback()
  }
}

const { Readable } = require('stream')

const source = Readable.from([
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 17 },
  { name: 'Charlie', age: 25 },
  { name: 'Diana', age: 15 }
])

const filter = new FilterStream(user => user.age >= 18)
const mapper = new MapStream(user => ({ ...user, adult: true }))

source
  .pipe(filter)
  .pipe(mapper)
  .on('data', (data) => console.log(data))

http/https 模块

创建 HTTP 服务器

Node.js HTTP Server 内部工作流程:

  ┌─────────────────────────────────────────────┐
  │             http.createServer()              │
  │                    │                         │
  │                    ▼                         │
  │            监听端口 (listen)                  │
  │                    │                         │
  │                    ▼                         │
  │        ┌───── 收到请求 ─────┐                │
  │        │                    │                │
  │        ▼                    ▼                │
  │   IncomingMessage      ServerResponse        │
  │   (req 可读流)          (res 可写流)          │
  │   method / url /       statusCode /          │
  │   headers / body       setHeader / write /   │
  │                        end                   │
  │        │                    │                │
  │        └──────┬─────────────┘                │
  │               ▼                              │
  │        执行请求处理函数                        │
  │               │                              │
  │               ▼                              │
  │        res.end() 发送响应                     │
  └─────────────────────────────────────────────┘

IncomingMessage 是一个 Readable 流,ServerResponse 是一个 Writable 流。这是理解 Node.js HTTP 模块的关键——请求和响应都是流。

js
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
  res.end('你好,Node.js')
})

server.listen(3000, '127.0.0.1', () => {
  console.log('服务器运行在 http://127.0.0.1:3000/')
})

请求/响应生命周期

一个完整的 HTTP 请求/响应在 Node.js 中经历以下阶段:

┌──────────────────────────────────────────────────────────┐
│               HTTP 请求/响应完整生命周期                    │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  客户端                          服务端                    │
│  ──────                          ──────                   │
│                                                          │
│  1. TCP 三次握手 ──────────────→  connection 事件          │
│                                                          │
│  2. 发送请求头   ──────────────→  request 事件触发         │
│     GET /api HTTP/1.1             解析为 IncomingMessage   │
│     Host: localhost               req.method = 'GET'      │
│     Accept: application/json      req.url = '/api'        │
│                                   req.headers = {...}     │
│                                                          │
│  3. 发送请求体   ──────────────→  req 'data' 事件          │
│     (POST/PUT)                    req 'end' 事件           │
│                                                          │
│  4.                               处理业务逻辑             │
│                                   res.writeHead(200)      │
│                                   res.write(chunk)        │
│                                                          │
│  5. 接收响应     ←──────────────  res.end()               │
│                                   response 'finish' 事件   │
│                                                          │
│  6. Keep-Alive 或 TCP 四次挥手                             │
└──────────────────────────────────────────────────────────┘

IncomingMessage 与 ServerResponse

请求体的读取必须通过流式方式,因为 IncomingMessage 是 Readable 流:

js
const http = require('http')

const server = http.createServer((req, res) => {
  const { method, url, headers, httpVersion } = req

  if (method === 'POST') {
    const chunks = []
    req.on('data', (chunk) => chunks.push(chunk))
    req.on('end', () => {
      const body = Buffer.concat(chunks).toString()
      res.writeHead(200, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ received: JSON.parse(body) }))
    })
    return
  }

  res.writeHead(200, {
    'Content-Type': 'application/json',
    'X-Request-Id': Date.now().toString()
  })
  res.end(JSON.stringify({ method, url, httpVersion }))
})

server.listen(3000)

Keep-Alive

HTTP/1.1 默认启用 Keep-Alive,即一个 TCP 连接可以复用多个 HTTP 请求/响应。Node.js 的 http.Server 默认支持 Keep-Alive,可以通过以下参数控制:

js
const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' })
  res.end('OK')
})

server.keepAliveTimeout = 5000
server.headersTimeout = 60000
server.maxHeadersCount = 2000
server.timeout = 120000

server.listen(3000)
属性默认值说明
keepAliveTimeout5000msKeep-Alive 连接的空闲超时时间
headersTimeout60000ms接收完整请求头的超时时间
timeout0(无限)整个请求的超时时间
maxHeadersCount2000最大请求头数量

在客户端使用 Keep-Alive:

js
const http = require('http')

const agent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 1000,
  maxSockets: 10,
  maxFreeSockets: 5
})

function request(url) {
  return new Promise((resolve, reject) => {
    const req = http.get(url, { agent }, (res) => {
      const chunks = []
      res.on('data', (chunk) => chunks.push(chunk))
      res.on('end', () => resolve(Buffer.concat(chunks).toString()))
    })
    req.on('error', reject)
  })
}

http.Agent 管理连接池,keepAlive: true 使得连接在请求完成后不会被关闭,而是放回池中供下次请求复用,避免了频繁的 TCP 三次握手开销。

手写简易静态文件服务器

js
const http = require('http')
const fs = require('fs')
const path = require('path')
const { pipeline } = require('stream')

const MIME_TYPES = {
  '.html': 'text/html; charset=utf-8',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
  '.txt': 'text/plain; charset=utf-8',
  '.woff2': 'font/woff2',
  '.mp4': 'video/mp4'
}

const STATIC_DIR = path.join(__dirname, 'public')

function getMimeType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  return MIME_TYPES[ext] || 'application/octet-stream'
}

const server = http.createServer((req, res) => {
  if (req.method !== 'GET') {
    res.writeHead(405)
    res.end('Method Not Allowed')
    return
  }

  const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname
  let filePath = path.join(STATIC_DIR, urlPath === '/' ? 'index.html' : urlPath)
  filePath = path.normalize(filePath)

  if (!filePath.startsWith(STATIC_DIR)) {
    res.writeHead(403)
    res.end('Forbidden')
    return
  }

  fs.stat(filePath, (err, stats) => {
    if (err || !stats.isFile()) {
      res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
      res.end('<h1>404 - 文件未找到</h1>')
      return
    }

    const mimeType = getMimeType(filePath)
    const headers = {
      'Content-Type': mimeType,
      'Content-Length': stats.size,
      'Cache-Control': 'public, max-age=3600',
      'ETag': `"${stats.size}-${stats.mtime.getTime()}"`,
      'Last-Modified': stats.mtime.toUTCString()
    }

    const ifNoneMatch = req.headers['if-none-match']
    const ifModifiedSince = req.headers['if-modified-since']

    if (ifNoneMatch === headers['ETag'] ||
        (ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime)) {
      res.writeHead(304, headers)
      res.end()
      return
    }

    res.writeHead(200, headers)
    pipeline(fs.createReadStream(filePath), res, (err) => {
      if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
        console.error(err)
      }
    })
  })
})

server.listen(8080, () => {
  console.log('静态文件服务器运行在 http://127.0.0.1:8080/')
})

这个实现包含了:路径遍历攻击防护(检查 startsWith)、ETag/Last-Modified 协商缓存、流式响应(避免大文件内存溢出)、pipeline 自动错误处理。


child_process 模块

exec/execFile/spawn/fork 四种方式对比

┌─────────────────────────────────────────────────────────────┐
│              child_process 四种方式对比                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  exec                    execFile                           │
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │ 创建 shell       │    │ 直接执行文件     │                │
│  │ 输出缓存到内存    │    │ 不创建 shell     │                │
│  │ 适合简短命令      │    │ 更安全更高效     │                │
│  │ 有 maxBuffer 限制│    │ 有 maxBuffer 限制│                │
│  └─────────────────┘    └─────────────────┘                │
│                                                             │
│  spawn                   fork                               │
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │ 流式输出         │    │ 专门fork Node进程│                │
│  │ 不创建 shell     │    │ 自带 IPC 通道    │                │
│  │ 适合大数据输出    │    │ 可发送消息和句柄  │                │
│  │ 最底层的 API     │    │ 基于 spawn 实现   │                │
│  └─────────────────┘    └─────────────────┘                │
└─────────────────────────────────────────────────────────────┘
方法shell输出方式IPC安全性适用场景
exec创建缓冲(Buffer/String)低(shell 注入)简单 shell 命令
execFile不创建缓冲(Buffer/String)直接执行可执行文件
spawn可选流(Stream)可选长时间运行、大输出
fork不创建流(Stream)自带Node.js 子进程通信

exec 与 execFile

js
const { exec, execFile } = require('child_process')

exec('ls -la /tmp | head -20', (error, stdout, stderr) => {
  if (error) {
    console.error(`执行错误: ${error.message}`)
    return
  }
  console.log(stdout)
})

execFile('node', ['--version'], (error, stdout) => {
  if (error) {
    console.error(error)
    return
  }
  console.log(`Node.js 版本: ${stdout.trim()}`)
})

exec 创建 shell 来执行命令,因此支持管道 |、重定向 >、通配符 * 等 shell 特性。但也正因为经过 shell,存在命令注入风险:

js
const { exec } = require('child_process')

const userInput = 'test; rm -rf /'

exec(`ls ${userInput}`)

上述代码中恶意输入会被 shell 解析为两条命令。使用 execFilespawn 可以避免此问题,因为参数是作为数组传入而非拼接成字符串。

spawn

js
const { spawn } = require('child_process')

const child = spawn('find', ['.', '-name', '*.js', '-type', 'f'])

child.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`)
})

child.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`)
})

child.on('close', (code) => {
  console.log(`子进程退出,退出码: ${code}`)
})

spawn 的流式特性适合管道组合:

js
const { spawn } = require('child_process')

const grep = spawn('grep', ['-r', 'TODO', './src'])
const wc = spawn('wc', ['-l'])

grep.stdout.pipe(wc.stdin)

wc.stdout.on('data', (data) => {
  console.log(`TODO 总数: ${data.toString().trim()}`)
})

IPC 通信

fork 是专门用于创建 Node.js 子进程的方法,它自动建立 IPC(Inter-Process Communication)通道。

┌─────────────┐     IPC 通道     ┌─────────────┐
│  父进程       │ ←─────────────→ │  子进程       │
│  (main.js)   │                 │  (worker.js) │
│              │   send()/on()   │              │
│  child.send  │ ────────────→   │ process.on   │
│  child.on    │ ←────────────   │ process.send │
└─────────────┘                  └─────────────┘

父进程 main.js:

js
const { fork } = require('child_process')
const path = require('path')

const worker = fork(path.join(__dirname, 'worker.js'))

worker.send({ type: 'compute', data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] })

worker.on('message', (result) => {
  console.log(`计算结果: ${result.sum}`)
  worker.kill()
})

worker.on('exit', (code) => {
  console.log(`子进程退出: ${code}`)
})

子进程 worker.js:

js
process.on('message', (msg) => {
  if (msg.type === 'compute') {
    const sum = msg.data.reduce((acc, val) => acc + val, 0)
    process.send({ sum })
  }
})

IPC 通信传递的数据会经过序列化/反序列化(类似 JSON),因此不能传递函数、Symbol 等不可序列化的值。对于大量数据传输,IPC 的开销不可忽略,可以考虑使用 SharedArrayBuffer。

cluster 模块与多进程

cluster 模块允许 Node.js 利用多核 CPU,通过 fork 多个工作进程来处理请求。它是构建在 child_process.fork 之上的高级抽象。

┌──────────────────────────────────────────────────────┐
│                 cluster 工作模式                       │
├──────────────────────────────────────────────────────┤
│                                                      │
│              ┌──────────────────┐                    │
│              │   Master 进程     │                    │
│              │   (不处理请求)     │                    │
│              │   管理 Worker     │                    │
│              └────────┬─────────┘                    │
│                       │                              │
│          ┌────────────┼────────────┐                 │
│          │            │            │                  │
│   ┌──────┴──────┐ ┌───┴──────┐ ┌──┴──────────┐      │
│   │  Worker 1   │ │ Worker 2 │ │  Worker 3   │      │
│   │  PID: 1001  │ │ PID: 1002│ │  PID: 1003  │      │
│   │  port: 3000 │ │ port:3000│ │  port: 3000 │      │
│   └─────────────┘ └──────────┘ └─────────────┘      │
│                       ↑                              │
│            所有 Worker 共享同一端口                     │
│            由 Master 通过 round-robin 分发             │
│            (Windows 除外,由操作系统调度)               │
└──────────────────────────────────────────────────────┘
js
const cluster = require('cluster')
const http = require('http')
const os = require('os')

const numCPUs = os.cpus().length

if (cluster.isMaster) {
  console.log(`Master ${process.pid} 启动`)

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} 退出 (${signal || code})`)
    console.log('启动新的 Worker...')
    cluster.fork()
  })

  cluster.on('online', (worker) => {
    console.log(`Worker ${worker.process.pid} 上线`)
  })
} else {
  http.createServer((req, res) => {
    res.writeHead(200)
    res.end(`Worker ${process.pid} 处理请求\n`)
  }).listen(3000)

  console.log(`Worker ${process.pid} 启动`)
}

cluster 的核心原理是端口共享:Master 进程创建 TCP 服务器并监听端口,当新连接到达时,Master 通过 IPC 将连接的文件描述符(fd)传递给某个 Worker,Worker 直接在该 fd 上进行读写。这避免了多进程监听同一端口的冲突。

生产环境中通常使用 PM2 来管理 cluster,它提供了零停机重启、日志管理、进程监控等功能:

┌─────────────────────────────────────────────────────┐
│                PM2 进程管理架构                        │
│                                                     │
│  ┌──────────┐                                       │
│  │  PM2 God │ ←── PM2 守护进程,管理所有应用           │
│  │  Daemon  │                                       │
│  └────┬─────┘                                       │
│       │                                             │
│  ┌────┴─────────────────────────────────────┐       │
│  │              App: my-server               │       │
│  │  ┌────────┐ ┌────────┐ ┌────────┐       │       │
│  │  │ inst 0 │ │ inst 1 │ │ inst 2 │  ...  │       │
│  │  │  fork  │ │  fork  │ │  fork  │       │       │
│  │  └────────┘ └────────┘ └────────┘       │       │
│  │                                          │       │
│  │  pm2 start app.js -i max                 │       │
│  │  pm2 reload app.js    (零停机重启)         │       │
│  └──────────────────────────────────────────┘       │
└─────────────────────────────────────────────────────┘

worker_threads 对比

child_process 创建的是独立进程,而 worker_threads 创建的是同一进程内的线程。两者适用于不同场景。

┌──────────────────────────────────────────────────────────┐
│              child_process vs worker_threads              │
│                                                          │
│  child_process (多进程)          worker_threads (多线程)   │
│  ┌──────────────────┐           ┌──────────────────┐     │
│  │ 进程 A           │           │ 进程              │     │
│  │ ┌──────────────┐ │           │ ┌──────┐┌──────┐ │     │
│  │ │ V8 实例      │ │           │ │主线程 ││Worker│ │     │
│  │ │ 独立堆内存   │ │           │ │V8实例 ││V8实例│ │     │
│  │ └──────────────┘ │           │ │      ││      │ │     │
│  └──────────────────┘           │ │      ││      │ │     │
│  ┌──────────────────┐           │ └──────┘└──────┘ │     │
│  │ 进程 B           │           │                  │     │
│  │ ┌──────────────┐ │           │ 共享进程内存空间   │     │
│  │ │ V8 实例      │ │           │ SharedArrayBuffer │     │
│  │ │ 独立堆内存   │ │           └──────────────────┘     │
│  │ └──────────────┘ │                                    │
│  └──────────────────┘                                    │
└──────────────────────────────────────────────────────────┘
维度child_processworker_threads
隔离级别进程级,完全隔离线程级,同一进程
内存各自独立堆内存可通过 SharedArrayBuffer 共享
启动开销高(创建新进程、新 V8 实例)较低(共享进程资源)
通信方式IPC(序列化)MessagePort + SharedArrayBuffer
稳定性子进程崩溃不影响主进程Worker 崩溃可能影响进程
适用场景CPU 密集任务、隔离第三方代码CPU 密集计算、共享数据处理
js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')

if (isMainThread) {
  const worker = new Worker(__filename, {
    workerData: { start: 1, end: 1000000 }
  })

  worker.on('message', (result) => {
    console.log(`素数个数: ${result}`)
  })

  worker.on('error', (err) => {
    console.error('Worker 错误:', err)
  })

  worker.on('exit', (code) => {
    console.log(`Worker 退出: ${code}`)
  })
} else {
  const { start, end } = workerData

  function isPrime(n) {
    if (n < 2) return false
    for (let i = 2; i <= Math.sqrt(n); i++) {
      if (n % i === 0) return false
    }
    return true
  }

  let count = 0
  for (let i = start; i <= end; i++) {
    if (isPrime(i)) count++
  }

  parentPort.postMessage(count)
}

使用 SharedArrayBuffer 实现零拷贝数据共享:

js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')

if (isMainThread) {
  const sharedBuffer = new SharedArrayBuffer(4 * 1024)
  const sharedArray = new Int32Array(sharedBuffer)

  for (let i = 0; i < sharedArray.length; i++) {
    sharedArray[i] = i
  }

  const worker = new Worker(__filename, {
    workerData: { sharedBuffer }
  })

  worker.on('message', () => {
    console.log('前 10 个元素:', Array.from(sharedArray.slice(0, 10)))
  })
} else {
  const { sharedBuffer } = workerData
  const sharedArray = new Int32Array(sharedBuffer)

  for (let i = 0; i < sharedArray.length; i++) {
    Atomics.add(sharedArray, i, 100)
  }

  parentPort.postMessage('done')
}

events 模块

EventEmitter 原理

Node.js 中几乎所有的异步 I/O 操作都基于事件驱动模型。EventEmitterevents 模块提供的核心类,它实现了观察者模式(发布-订阅模式),是 Node.js 事件驱动架构的基石。

┌──────────────────────────────────────────────────────┐
│              EventEmitter 内部结构                     │
│                                                      │
│  内部维护一个事件 → 监听器映射表:                       │
│                                                      │
│  _events = {                                         │
│    'data':    [handler1, handler2, handler3],         │
│    'error':   [errorHandler],                        │
│    'end':     [endHandler1, endHandler2],             │
│    'close':   [closeHandler]                         │
│  }                                                   │
│                                                      │
│  emit('data', payload)                               │
│       │                                              │
│       ├──→ handler1(payload)  同步依次调用             │
│       ├──→ handler2(payload)                         │
│       └──→ handler3(payload)                         │
│                                                      │
│  关键特性:                                            │
│  - 监听器按注册顺序同步执行                             │
│  - emit 返回 boolean 表示是否有监听器                   │
│  - error 事件无监听器时抛出异常                         │
└──────────────────────────────────────────────────────┘

核心 API

js
const EventEmitter = require('events')

class DataProcessor extends EventEmitter {
  process(data) {
    this.emit('start')
    const result = data.map(item => item * 2)
    this.emit('data', result)
    this.emit('end', { count: result.length })
  }
}

const processor = new DataProcessor()

processor.on('start', () => console.log('处理开始'))
processor.on('data', (result) => console.log('处理结果:', result))
processor.on('end', (info) => console.log(`处理完成,共 ${info.count} 条`))

processor.process([1, 2, 3, 4, 5])

EventEmitter 完整 API 速查:

方法作用别名
on(event, fn)注册监听器addListener
once(event, fn)注册一次性监听器-
off(event, fn)移除指定监听器removeListener
emit(event, ...args)触发事件-
removeAllListeners([event])移除所有监听器-
listenerCount(event)获取监听器数量-
eventNames()获取所有事件名-
prependListener(event, fn)在头部插入监听器-
setMaxListeners(n)设置最大监听器数量-

错误处理

error 事件在 EventEmitter 中具有特殊地位:如果 emit('error') 时没有注册任何 error 监听器,Node.js 会直接抛出异常并退出进程。这是 EventEmitter 唯一一个有特殊语义的事件。

js
const EventEmitter = require('events')

const emitter = new EventEmitter()

emitter.emit('error', new Error('未处理的错误'))

emitter.on('error', (err) => {
  console.error('捕获到错误:', err.message)
})
emitter.emit('error', new Error('已处理的错误'))

在生产环境中,始终为 EventEmitter 实例注册 error 监听器是最佳实践。对于使用了 EventEmitter 的类(如 Stream、HTTP Server),都应该监听 error 事件。

内存泄漏排查(MaxListenersExceededWarning)

当一个事件注册的监听器数量超过默认上限(10 个)时,Node.js 会发出 MaxListenersExceededWarning 警告。这通常意味着存在内存泄漏——代码可能在循环或重复调用中不断添加监听器而未移除。

js
const EventEmitter = require('events')
const emitter = new EventEmitter()

for (let i = 0; i < 20; i++) {
  emitter.on('data', () => {})
}

运行上述代码会看到警告:

MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter].
Use emitter.setMaxListeners() to increase limit

常见的内存泄漏场景:

js
const EventEmitter = require('events')

class Connection extends EventEmitter {}
const conn = new Connection()

function handleRequest(req) {
  conn.on('data', (data) => {
    req.respond(data)
  })
}

上面的代码在每次 handleRequest 被调用时都会新增一个 data 监听器,但从不移除。随着请求量增加,监听器不断累积,导致内存泄漏和性能下降。

修复方案:

js
const EventEmitter = require('events')

class Connection extends EventEmitter {}
const conn = new Connection()

function handleRequest(req) {
  function onData(data) {
    req.respond(data)
    conn.removeListener('data', onData)
  }
  conn.on('data', onData)
}

function handleRequestOnce(req) {
  conn.once('data', (data) => {
    req.respond(data)
  })
}

排查工具与方法:

js
const EventEmitter = require('events')

EventEmitter.defaultMaxListeners = 15

const emitter = new EventEmitter()
emitter.setMaxListeners(20)

console.log(emitter.listenerCount('data'))
console.log(emitter.eventNames())
console.log(emitter.listeners('data'))

process.on('warning', (warning) => {
  if (warning.name === 'MaxListenersExceededWarning') {
    console.error('发现潜在内存泄漏:', warning.message)
    console.trace()
  }
})

手写 EventEmitter 实现

js
class SimpleEventEmitter {
  constructor() {
    this._events = Object.create(null)
    this._maxListeners = 10
  }

  on(eventName, listener) {
    if (typeof listener !== 'function') {
      throw new TypeError('监听器必须是函数')
    }

    if (!this._events[eventName]) {
      this._events[eventName] = []
    }

    if (this._events[eventName].length >= this._maxListeners && this._maxListeners > 0) {
      console.warn(
        `MaxListenersExceededWarning: ${eventName} 事件已有 ` +
        `${this._events[eventName].length} 个监听器`
      )
    }

    this._events[eventName].push(listener)
    return this
  }

  once(eventName, listener) {
    const wrapper = (...args) => {
      listener.apply(this, args)
      this.off(eventName, wrapper)
    }
    wrapper._original = listener
    return this.on(eventName, wrapper)
  }

  emit(eventName, ...args) {
    const listeners = this._events[eventName]
    if (!listeners || listeners.length === 0) {
      if (eventName === 'error') {
        throw args[0] instanceof Error ? args[0] : new Error('Unhandled error')
      }
      return false
    }

    const copied = [...listeners]
    for (const fn of copied) {
      fn.apply(this, args)
    }
    return true
  }

  off(eventName, listener) {
    const listeners = this._events[eventName]
    if (!listeners) return this

    this._events[eventName] = listeners.filter(
      (fn) => fn !== listener && fn._original !== listener
    )

    if (this._events[eventName].length === 0) {
      delete this._events[eventName]
    }
    return this
  }

  removeAllListeners(eventName) {
    if (eventName) {
      delete this._events[eventName]
    } else {
      this._events = Object.create(null)
    }
    return this
  }

  listenerCount(eventName) {
    const listeners = this._events[eventName]
    return listeners ? listeners.length : 0
  }

  eventNames() {
    return Object.keys(this._events)
  }

  setMaxListeners(n) {
    this._maxListeners = n
    return this
  }

  prependListener(eventName, listener) {
    if (!this._events[eventName]) {
      this._events[eventName] = []
    }
    this._events[eventName].unshift(listener)
    return this
  }
}

实现要点分析:

  • _events 使用 Object.create(null) 创建无原型的纯净对象,避免原型链上的属性干扰
  • emit 中先复制监听器数组再遍历,防止在回调中添加/移除监听器导致迭代异常
  • once 通过 wrapper._original 保存原始函数引用,使 off 时传入原始函数也能正确移除
  • error 事件无监听器时直接 throw,与 Node.js 原生行为一致

Buffer 与二进制数据

Buffer 概述

Buffer 是 Node.js 中处理二进制数据的核心类。JavaScript 原生的字符串是以 UTF-16 编码存储的,无法直接处理原始二进制数据(如图片、音频、网络数据包),Buffer 正是为此而生。

┌─────────────────────────────────────────────────────┐
│                  Buffer 在内存中的位置                 │
├─────────────────────────────────────────────────────┤
│                                                     │
│  V8 堆内存                                          │
│  ┌──────────────────────────────┐                   │
│  │ String / Number / Object    │                   │
│  │ Array / Function            │                   │
│  │ 受 V8 GC 管理               │                   │
│  └──────────────────────────────┘                   │
│                                                     │
│  ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│                                                     │
│  V8 堆外内存(C++ 层分配)                            │
│  ┌──────────────────────────────┐                   │
│  │         Buffer               │                   │
│  │  固定大小的原始内存块           │                   │
│  │  底层是 ArrayBuffer          │                   │
│  │  不受 V8 堆大小限制           │                   │
│  │  通过 GC + C++ Release 回收  │                   │
│  └──────────────────────────────┘                   │
└─────────────────────────────────────────────────────┘

Buffer 存储在 V8 堆外内存中(通过 C++ 层的 ArrayBuffer 分配),这意味着 Buffer 的大小不受 --max-old-space-size 限制。但 Buffer 的 JS 对象壳(存储 length、byteOffset 等元信息的对象)仍在 V8 堆中,当 JS 壳被 GC 回收时,会触发 C++ 层释放对应的堆外内存。

Buffer 创建与转换

js
const buf1 = Buffer.alloc(10)
const buf2 = Buffer.alloc(10, 0xff)
const buf3 = Buffer.allocUnsafe(10)
const buf4 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f])
const buf5 = Buffer.from('你好世界', 'utf-8')
const buf6 = Buffer.from('SGVsbG8=', 'base64')

console.log(buf4.toString('utf-8'))
console.log(buf5.toString('hex'))
console.log(buf5.toString('base64'))

console.log(buf5.length)
console.log('你好世界'.length)

buf5.length 是 12(UTF-8 编码下每个中文字符占 3 字节),而字符串的 length 是 4。这是 Buffer 和 String 的核心区别——Buffer 的 length 是字节数,String 的 length 是字符数。

alloc vs allocUnsafe 的区别:

方法初始化性能安全性
Buffer.alloc(size)零填充较慢安全
Buffer.allocUnsafe(size)不初始化较快可能包含旧数据

allocUnsafe 更快是因为跳过了内存清零步骤,但分配的内存中可能残留之前的敏感数据。在不会立即填充的场景下应使用 alloc

Buffer 操作

js
const buf = Buffer.from('Hello')

const slice = buf.subarray(0, 3)
slice[0] = 0x4a
console.log(buf.toString())

const combined = Buffer.concat([
  Buffer.from('Hello '),
  Buffer.from('World')
])
console.log(combined.toString())

console.log(Buffer.from('abc').equals(Buffer.from('abc')))
console.log(Buffer.compare(Buffer.from('a'), Buffer.from('b')))

subarray 返回的是原 Buffer 的视图,共享同一块底层内存,修改 slice 会影响原 Buffer。如果需要独立副本,使用 Buffer.from(buf)

二进制协议解析是 Buffer 的典型应用场景:

js
function parsePacket(buf) {
  const version = buf.readUInt8(0)
  const type = buf.readUInt8(1)
  const length = buf.readUInt16BE(2)
  const payload = buf.subarray(4, 4 + length)

  return { version, type, length, payload }
}

function buildPacket(version, type, payload) {
  const header = Buffer.alloc(4)
  header.writeUInt8(version, 0)
  header.writeUInt8(type, 1)
  header.writeUInt16BE(payload.length, 2)

  return Buffer.concat([header, payload])
}

const packet = buildPacket(1, 2, Buffer.from('Hello'))
console.log(parsePacket(packet))

编码处理(UTF-8/Base64)

js
const str = 'Hello 你好'

const utf8Buf = Buffer.from(str, 'utf-8')
console.log(utf8Buf)
console.log(utf8Buf.length)

const base64Str = utf8Buf.toString('base64')
console.log(base64Str)

const fromBase64 = Buffer.from(base64Str, 'base64')
console.log(fromBase64.toString('utf-8'))

const hexStr = utf8Buf.toString('hex')
console.log(hexStr)

Node.js 支持的编码格式:

编码说明用途
utf-8变长编码,ASCII 用 1 字节,中文用 3 字节默认编码,文本文件
ascii7 位编码,0-127纯英文字符
base64每 3 字节编码为 4 字符,体积增加 33%二进制数据文本化传输
base64urlURL 安全的 Base64(+/ 替换为 -_JWT、URL 参数
hex每字节编码为 2 个十六进制字符哈希值、调试
binary/latin1单字节编码遗留系统兼容

流式读取中的编码陷阱——多字节字符被截断:

js
const { StringDecoder } = require('string_decoder')

const decoder = new StringDecoder('utf-8')

const buf1 = Buffer.from([0xe4, 0xbd])
const buf2 = Buffer.from([0xa0, 0xe5, 0xa5, 0xbd])

console.log(buf1.toString())
console.log(buf2.toString())

console.log(decoder.write(buf1))
console.log(decoder.write(buf2))

直接 toString() 会因为 UTF-8 多字节字符被分割到两个 chunk 中而产生乱码。StringDecoder 会缓存不完整的字节序列,等到下一个 chunk 到来时拼接出完整字符。readline 模块和 Readable 流的 setEncoding() 内部都使用了 StringDecoder

ArrayBuffer/TypedArray 关系

┌─────────────────────────────────────────────────────────┐
│                ArrayBuffer 体系                          │
│                                                         │
│  ┌─────────────┐                                        │
│  │ ArrayBuffer │ ←── 底层二进制数据容器(不可直接操作)     │
│  └──────┬──────┘                                        │
│         │                                               │
│         │  通过视图(View)操作数据                       │
│         │                                               │
│    ┌────┴────────────────────────────┐                   │
│    │         TypedArray              │                   │
│    │  Uint8Array / Int32Array /      │                   │
│    │  Float64Array / ...             │                   │
│    └────────────────┬────────────────┘                   │
│                     │                                    │
│                     │  Buffer 是 Uint8Array 的子类        │
│                     │                                    │
│    ┌────────────────┴────────────────┐                   │
│    │           Buffer                │                   │
│    │  继承 Uint8Array 全部能力       │                   │
│    │  额外方法: toString(encoding)   │                   │
│    │  write / copy / readInt32LE    │                   │
│    └─────────────────────────────────┘                   │
└─────────────────────────────────────────────────────────┘
js
const buf = Buffer.from([1, 2, 3, 4])

console.log(buf instanceof Uint8Array)
console.log(buf instanceof Buffer)

const uint8 = new Uint8Array([1, 2, 3, 4])
const sharedBuf = Buffer.from(uint8.buffer)
uint8[0] = 99
console.log(sharedBuf[0])

const copiedBuf = Buffer.from(uint8)
uint8[0] = 200
console.log(copiedBuf[0])

Buffer.from(typedArray.buffer) 传入底层 ArrayBuffer,创建的 Buffer 与 TypedArray 共享内存Buffer.from(typedArray) 传入 TypedArray 本身,会复制数据到新内存。


process 模块

概述

process 是一个全局对象(无需 require),它提供了当前 Node.js 进程的信息和控制能力。它是 EventEmitter 的实例,也是 Node.js 与操作系统交互的桥梁。

┌─────────────────────────────────────────────────────────┐
│                  process 对象核心能力                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  进程信息                                                │
│  ├── process.pid           当前进程 PID                  │
│  ├── process.ppid          父进程 PID                    │
│  ├── process.title         进程标题(ps 命令可见)         │
│  ├── process.argv          命令行参数数组                 │
│  ├── process.execPath      Node.js 可执行文件路径         │
│  ├── process.version       Node.js 版本                  │
│  ├── process.versions      V8/libuv/OpenSSL 等版本       │
│  ├── process.platform      操作系统平台                   │
│  └── process.arch          CPU 架构                      │
│                                                         │
│  运行时控制                                               │
│  ├── process.cwd()         当前工作目录                   │
│  ├── process.chdir(dir)    切换工作目录                   │
│  ├── process.exit([code])  退出进程                       │
│  ├── process.abort()       立即终止(生成 core dump)      │
│  ├── process.kill(pid)     发送信号                       │
│  └── process.uptime()      进程运行时长(秒)              │
│                                                         │
│  内存与性能                                               │
│  ├── process.memoryUsage() 内存使用详情                   │
│  ├── process.cpuUsage()    CPU 使用时间                   │
│  └── process.hrtime.bigint() 高精度时间戳                 │
│                                                         │
│  I/O 流                                                  │
│  ├── process.stdin         标准输入(Readable 流)         │
│  ├── process.stdout        标准输出(Writable 流)         │
│  └── process.stderr        标准错误(Writable 流)         │
└─────────────────────────────────────────────────────────┘

环境变量

process.env 是一个包含所有环境变量的对象。需要注意的是,所有的值都是字符串类型。

js
console.log(process.env.NODE_ENV)
console.log(process.env.PORT)
console.log(process.env.HOME)
console.log(process.env.PATH)

process.env.MY_VAR = 'hello'
console.log(process.env.MY_VAR)

process.env.DEBUG_MODE = 'true'
console.log(process.env.DEBUG_MODE === true)
console.log(process.env.DEBUG_MODE === 'true')

环境变量的值始终是字符串,process.env.DEBUG_MODE = true 会被隐式转换为 'true'。判断时必须与字符串比较。

生产环境中配置管理的最佳实践:

js
function getConfig() {
  return {
    port: parseInt(process.env.PORT, 10) || 3000,
    host: process.env.HOST || '0.0.0.0',
    nodeEnv: process.env.NODE_ENV || 'development',
    dbUrl: process.env.DATABASE_URL,
    isProduction: process.env.NODE_ENV === 'production',
    logLevel: process.env.LOG_LEVEL || 'info'
  }
}

function validateConfig(config) {
  const required = ['dbUrl']
  const missing = required.filter(key => !config[key])
  if (missing.length > 0) {
    throw new Error(`缺少必要的环境变量: ${missing.join(', ')}`)
  }
  return config
}

退出码

process.exit(code) 终止进程并返回退出码。退出码 0 表示正常退出,非零值表示异常。

js
process.exit(0)
process.exit(1)

process.exitCode = 1

常见退出码含义:

退出码含义
0正常退出
1未捕获的致命异常(Uncaught Fatal Exception)
2未使用(Bash 保留)
3内部 JavaScript 解析错误
5V8 致命错误
9无效参数
12无效的调试参数
13未完成的 Top-Level Await
>128信号退出(128 + 信号编号)

推荐使用 process.exitCode 而非直接调用 process.exit(),因为后者会立即终止进程,可能导致 stdout 缓冲区中的数据丢失(stdout 是异步的):

js
process.stdout.write('重要日志数据')
process.exit(0)

process.stdout.write('重要日志数据', () => {
  process.exitCode = 0
})

信号处理

Node.js 进程可以接收操作系统发送的信号,常见信号:

┌──────────────────────────────────────────────────────┐
│                   常用 POSIX 信号                      │
├──────────┬──────────┬────────────────────────────────┤
│  信号     │  编号    │  说明                           │
├──────────┼──────────┼────────────────────────────────┤
│  SIGTERM  │  15      │  请求终止(可拦截),默认行为     │
│  SIGINT   │  2       │  Ctrl+C 中断                   │
│  SIGHUP   │  1       │  终端关闭                       │
│  SIGUSR1  │  10      │  用户自定义(Node 用于调试器)    │
│  SIGUSR2  │  12      │  用户自定义                     │
│  SIGKILL  │  9       │  强制终止(不可拦截)             │
│  SIGSTOP  │  19      │  暂停进程(不可拦截)             │
└──────────┴──────────┴────────────────────────────────┘

优雅关闭(Graceful Shutdown)是生产环境的必备能力:

js
const http = require('http')

const server = http.createServer((req, res) => {
  res.end('OK')
})

server.listen(3000)

let isShuttingDown = false

async function gracefulShutdown(signal) {
  if (isShuttingDown) return
  isShuttingDown = true

  console.log(`收到 ${signal} 信号,开始优雅关闭...`)

  server.close(() => {
    console.log('HTTP 服务器已关闭')
  })

  setTimeout(() => {
    console.error('强制退出:关闭超时')
    process.exit(1)
  }, 10000).unref()
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))

.unref() 使定时器不会阻止进程退出——如果所有连接都已关闭,进程不需要等待 10 秒超时。

未捕获异常与未处理的 Promise 拒绝

js
process.on('uncaughtException', (err, origin) => {
  console.error(`未捕获异常 [${origin}]:`, err)
  process.exitCode = 1
})

process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 拒绝:', reason)
})

process.on('warning', (warning) => {
  console.warn('进程警告:', warning.name, warning.message)
})

uncaughtException 是最后的安全网。但注意:在捕获到未知异常后,进程的状态可能已经不一致(文件句柄泄漏、内存损坏等),最佳实践是记录错误日志后尽快重启进程,而不是继续运行。

process.nextTick vs setImmediate

这两个调度函数在 Node.js 事件循环中有本质区别:

┌─────────────────────────────────────────────────────────┐
│          process.nextTick vs setImmediate                │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  事件循环一个 tick 的执行顺序:                             │
│                                                         │
│  ┌─────────────┐                                        │
│  │ 当前同步代码  │                                        │
│  └──────┬──────┘                                        │
│         ▼                                               │
│  ┌─────────────┐                                        │
│  │ nextTick    │ ← 在当前阶段结束后、下一阶段开始前执行     │
│  │ 队列        │   属于微任务,优先级最高                   │
│  └──────┬──────┘                                        │
│         ▼                                               │
│  ┌─────────────┐                                        │
│  │ Promise     │ ← 微任务队列,优先级次于 nextTick         │
│  │ .then 队列  │                                         │
│  └──────┬──────┘                                        │
│         ▼                                               │
│  ┌─────────────┐                                        │
│  │ 事件循环     │ ← timers → poll → check(setImmediate)  │
│  │ 下一阶段     │                                        │
│  └─────────────┘                                        │
│                                                         │
│  nextTick: 插队机制,在两个阶段之间执行                     │
│  setImmediate: 在 check 阶段排队执行                      │
└─────────────────────────────────────────────────────────┘
维度process.nextTicksetImmediate
执行时机当前操作完成后、事件循环下一阶段前事件循环的 check 阶段
优先级最高(高于 Promise.then)低于微任务
递归调用会饿死 I/O(阻塞事件循环)不会饿死 I/O
实现位置V8 微任务队列libuv check 阶段
js
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise.then'))
console.log('同步代码')

输出顺序:同步代码nextTickPromise.thensetImmediate

递归 nextTick 饿死 I/O 的示例:

js
const fs = require('fs')

fs.readFile(__filename, () => {
  console.log('文件读取完成')
})

function recursiveNextTick() {
  process.nextTick(recursiveNextTick)
}
recursiveNextTick()

上述代码中,文件读取完成的回调永远不会执行,因为 nextTick 队列永远不为空,事件循环无法进入 poll 阶段。解决方案是用 setImmediate 替代递归 nextTick

内存监控

js
function formatMemory(bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB'
}

function printMemoryUsage() {
  const mem = process.memoryUsage()
  console.log({
    rss: formatMemory(mem.rss),
    heapTotal: formatMemory(mem.heapTotal),
    heapUsed: formatMemory(mem.heapUsed),
    external: formatMemory(mem.external),
    arrayBuffers: formatMemory(mem.arrayBuffers)
  })
}

printMemoryUsage()
字段含义
rss常驻内存集(Resident Set Size),进程占用的物理内存总量
heapTotalV8 堆内存总量
heapUsedV8 堆内存使用量
externalV8 管理的 C++ 对象绑定的内存(如 Buffer 的 JS 壳)
arrayBuffersArrayBuffer 和 SharedArrayBuffer 占用的内存

高精度性能计时:

js
const start = process.hrtime.bigint()

let sum = 0
for (let i = 0; i < 1000000; i++) {
  sum += i
}

const end = process.hrtime.bigint()
console.log(`耗时: ${Number(end - start) / 1e6} ms`)

process.hrtime.bigint() 返回纳秒精度的时间戳,远比 Date.now() 精确,适合性能基准测试。


面试高频问题

题目一:fs 模块中 readFile 和 createReadStream 的核心区别

:处理一个 2GB 的日志文件应该用哪个 API?为什么?

:必须使用 createReadStreamreadFile 将整个文件一次性读入内存,而 createReadStream 以流的方式分块读取,每次只在内存中保持一个 highWaterMark 大小的数据块(默认 64KB)。

V8 堆内存默认限制约 1.7GB,readFile 读取 2GB 文件会导致内存溢出。即使通过 --max-old-space-size 扩大堆内存,一次性分配 2GB 的连续内存也可能因内存碎片而失败。流式处理可以在恒定的内存占用(约 64KB + 写入缓冲区)下完成整个文件的处理。

js
const fs = require('fs')
const readline = require('readline')

async function analyzeLog(filePath) {
  const rl = readline.createInterface({
    input: fs.createReadStream(filePath),
    crlfDelay: Infinity
  })

  const stats = { total: 0, errors: 0, warnings: 0 }

  for await (const line of rl) {
    stats.total++
    if (line.includes('ERROR')) stats.errors++
    if (line.includes('WARN')) stats.warnings++
  }

  return stats
}

题目二:path.resolve 和 path.join 的区别

path.resolve('a', '/b', 'c')path.join('a', '/b', 'c') 的结果分别是什么?

  • path.resolve('a', '/b', 'c')/b/c
  • path.join('a', '/b', 'c')a/b/c

path.resolve 从右向左处理参数,遇到绝对路径即停止拼接——处理到 /b 时发现是绝对路径,忽略左侧的 'a',然后拼接 'c',返回 /b/cpath.join 则不关心参数是否是绝对路径,纯粹做字符串拼接并规范化。

核心区别:resolve 总是返回绝对路径(未遇到绝对路径时以 process.cwd() 为基准),join 可能返回相对路径。

题目三:什么是背压(Backpressure)

:在 Node.js 流中如何处理背压?不处理会怎样?

:背压是当 Readable 产生数据的速度超过 Writable 处理速度时产生的压力。

处理方式:检测 writable.write(chunk) 的返回值——当返回 false 时,说明 Writable 的内部缓冲区已达到 highWaterMark,此时应调用 readable.pause() 暂停读取;监听 Writable 的 drain 事件,缓冲区排空后调用 readable.resume() 恢复读取。

不处理背压的后果:数据在 Writable 的内部缓冲区中无限堆积,内存持续增长直到 OOM。例如从快速 SSD 读取文件写入慢速网络 socket,不处理背压时数据会大量堆积在内存中。

推荐使用 stream.pipeline() 自动处理背压,它还会在出错时自动销毁所有流,防止内存泄漏。

题目四:process.nextTick 和 setImmediate 的区别

:为什么递归调用 nextTick 会饿死 I/O?

process.nextTick 的回调在当前操作完成后、事件循环进入下一阶段之前立即执行,属于微任务且优先级最高(高于 Promise.then)。setImmediate 的回调在事件循环的 check 阶段执行。

递归调用 nextTick 会在每个阶段切换时不断插入新的微任务,而事件循环在进入下一阶段前必须清空所有微任务队列。这导致事件循环永远停留在微任务清理阶段,无法进入 poll 阶段处理 I/O 回调。

js
function recursive() {
  process.nextTick(recursive)
}
recursive()

setTimeout(() => console.log('永远不会执行'), 0)

在 I/O 回调中,setImmediate 确定先于 setTimeout(fn, 0) 执行:

js
const fs = require('fs')

fs.readFile(__filename, () => {
  setImmediate(() => console.log('setImmediate'))
  setTimeout(() => console.log('setTimeout'), 0)
})

题目五:手写 EventEmitter 的核心考点

:手写 EventEmitter 需要注意哪些边界情况?

  1. error 事件特殊处理emit('error') 无监听器时必须抛出异常,否则错误会被静默吞掉
  2. once 的实现:通过包装函数实现一次性监听,但 off 时传入原始函数也需要能正确移除——通过 wrapper._original 保存引用
  3. emit 中的数组复制:遍历监听器前先复制数组 [...listeners],防止回调中执行 off 导致数组长度变化跳过监听器
  4. MaxListenersExceededWarning:监听器数量超过阈值时发出警告,帮助排查内存泄漏
  5. 返回 thisonoffonce 等方法返回 this 以支持链式调用
js
const emitter = new SimpleEventEmitter()

emitter
  .on('a', () => console.log(1))
  .on('a', () => console.log(2))
  .once('b', () => console.log(3))

题目六:Buffer.alloc 和 Buffer.allocUnsafe 的区别

:什么场景下应该用 allocUnsafe?有什么风险?

Buffer.alloc(size) 会将分配的内存初始化为零,Buffer.allocUnsafe(size) 跳过初始化步骤。allocUnsafe 更快(在分配大 Buffer 时差异明显),但分配的内存中可能残留之前释放的数据(包括密码、密钥等敏感信息)。

适合使用 allocUnsafe 的场景:分配后立即完全填充的情况,如 const buf = Buffer.allocUnsafe(size); file.read(buf, 0, size)——因为读取操作会覆盖所有字节。

不应使用 allocUnsafe 的场景:Buffer 可能只被部分填充,或者直接被传输出去(如发送到网络),此时未填充的区域可能泄露旧数据。

题目七:child_process.exec 的安全风险

:为什么不应该用 exec 执行包含用户输入的命令?

exec 通过 shell 执行命令,用户输入会被 shell 解释。攻击者可以通过分号 ;、管道 |、反引号 ` 等 shell 元字符注入恶意命令:

js
const { exec, execFile } = require('child_process')

const filename = 'test.txt; cat /etc/passwd'

exec(`cat ${filename}`)

execFile('cat', [filename], (err, stdout) => {
  if (err) console.error(err)
})

exec 会将整个字符串交给 shell 解析,分号后的 cat /etc/passwd 会被作为第二条命令执行。execFile 将参数作为数组传递给可执行文件,不经过 shell 解释,因此 ; cat /etc/passwd 只是一个普通的文件名参数,会提示文件不存在。

规则:凡是涉及用户输入的命令执行,一律使用 execFilespawn,避免 shell 注入。

题目八:Node.js 如何利用多核 CPU

:Node.js 是单线程的,如何在多核服务器上充分利用 CPU 资源?

:Node.js 提供了三种方式利用多核 CPU:

  1. cluster 模块:Master 进程 fork 出多个 Worker 进程,所有 Worker 共享同一端口。Master 通过 round-robin(Linux/macOS)或操作系统调度(Windows)将连接分发给 Worker。每个 Worker 是独立的 Node.js 进程,拥有独立的 V8 实例和事件循环。

  2. worker_threads 模块:在同一进程内创建多个 JS 执行线程,线程之间可以通过 SharedArrayBuffer 共享内存,避免了进程间通信的序列化开销。适合 CPU 密集型计算任务。

  3. child_process 模块:fork 出子进程执行特定任务,通过 IPC 通信。适合需要进程级隔离的场景。

选型建议:Web 服务器优先使用 cluster(或 PM2 的 cluster mode);CPU 密集计算优先使用 worker_threads;需要执行外部程序或需要强隔离时使用 child_process

选型决策树:

  需要利用多核?

       ├── Web 服务器 ──→ cluster / PM2

       ├── CPU 密集计算 ──→ worker_threads
       │                   (共享内存,低开销)

       └── 执行外部程序 / 强隔离 ──→ child_process
                                    (独立进程,安全)

延伸阅读

学习路径推荐

┌──────────────────────────────────────────────────────────┐
│               Node.js 核心模块学习路径                      │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  第一阶段:基础掌握                                        │
│  ├── fs 模块:文件读写、目录操作、fs/promises              │
│  ├── path 模块:路径拼接、解析、跨平台                      │
│  ├── events 模块:EventEmitter、on/emit/once              │
│  └── Buffer 模块:创建、转换、编码                          │
│                                                          │
│  第二阶段:核心进阶                                        │
│  ├── stream 模块:四种流类型、背压、pipeline               │
│  ├── http 模块:创建服务器、请求生命周期                    │
│  ├── process 模块:信号处理、环境变量                       │
│  └── child_process 模块:spawn/fork/IPC                   │
│                                                          │
│  第三阶段:深入原理                                        │
│  ├── libuv 事件循环源码阅读                                │
│  ├── V8 内存管理与 GC 机制                                │
│  ├── cluster 与 worker_threads 多核利用                   │
│  └── 手写核心模块简易实现(EventEmitter/Stream)            │
│                                                          │
│  第四阶段:生产实践                                        │
│  ├── 性能调优(profiling、内存泄漏排查)                    │
│  ├── 优雅关闭与进程管理(PM2/systemd)                     │
│  ├── 日志、监控、APM 集成                                  │
│  └── 安全实践(输入校验、依赖审计)                         │
└──────────────────────────────────────────────────────────┘

推荐资源

官方文档

深入原理

  • 《Node.js 设计模式》(Node.js Design Patterns)——系统讲解 Node.js 的设计哲学和最佳实践,涵盖流、事件驱动、进程管理等核心主题
  • 《深入浅出 Node.js》(朴灵著)——国内经典,深入讲解 V8、libuv、Buffer、Stream 等底层原理
  • libuv 官方文档(https://docs.libuv.org/)——理解事件循环和异步 I/O 的底层实现

源码阅读

  • Node.js GitHub 仓库(https://github.com/nodejs/node)——`lib/` 目录是 JS 层核心模块实现,src/ 目录是 C++ 层实现
  • 重点阅读:lib/internal/streams/ 目录(Stream 实现)、lib/fs.js(fs 模块 JS 层)、lib/events.js(EventEmitter 实现)

实战项目建议

  • 手写一个支持路由、中间件的迷你 HTTP 框架(深入理解 http 模块和 Stream)
  • 实现一个文件监听 + 自动重启的开发工具(深入理解 fs.watch 和 child_process)
  • 构建一个多进程日志处理管道(深入理解 Stream、child_process、cluster)
  • 实现一个简易的 RPC 框架(深入理解 net 模块、Buffer 协议解析、EventEmitter)

用心学习,用代码说话 💻