Skip to content

EventEmitter、Buffer 与 Stream

Node 处理数据的三大基础抽象,从事件到字节再到流。

EventEmitter

Node 很多核心模块都建立在事件驱动模型上,EventEmitter 是这个模型的基础设施。

js
import { EventEmitter } from 'events'

const emitter = new EventEmitter()

// 注册监听器
emitter.on('data', (chunk) => {
  console.log('收到数据:', chunk)
})

// 只监听一次
emitter.once('connect', () => {
  console.log('已连接')
})

// 触发事件
emitter.emit('data', { id: 1, value: 'hello' })

// 移除监听器(防止内存泄漏)
const handler = () => {}
emitter.on('event', handler)
emitter.off('event', handler)  // 或 emitter.removeListener('event', handler)

非常重要:emit() 触发时,监听器是同步调用的。

js
emitter.on('task', () => {
  // 这里的重 CPU 计算会直接拖慢当前流程
  heavyCompute()
})
emitter.emit('task')  // 同步执行监听器

"事件产生是异步的"不等于"监听器执行是异步的"。很多人误以为 EventEmitter 天然是异步的。

内存泄漏警告:

js
// Node 默认一个事件超过 10 个监听器会警告
emitter.setMaxListeners(20)  // 如果确实需要更多监听器

Buffer

Buffer 用来表示一段二进制数据,本质上是字节序列。继承自 Uint8Array

为什么服务端必须理解 Buffer:

  • 网络传输的数据底层都是字节
  • 文件不是天然字符串
  • 不同编码之间要明确转换
  • 大文件、流式处理、协议解析都离不开 Buffer
js
// 创建 Buffer
const buf1 = Buffer.from('Hello, 世界', 'utf-8')
const buf2 = Buffer.alloc(10)          // 10 字节,填充 0
const buf3 = Buffer.allocUnsafe(10)    // 10 字节,不初始化(更快,但需注意旧数据)

// 转换
buf1.toString('utf-8')      // 'Hello, 世界'
buf1.toString('base64')     // base64 编码
buf1.toString('hex')        // 十六进制字符串

// 拼接
const merged = Buffer.concat([buf1, buf2])

// 读写
buf2.writeUInt32BE(123456, 0)  // 写 4 字节无符号整数(大端序)
buf2.readUInt32BE(0)            // 读回

常见误区:

js
// ❌ 错误:把 Buffer 当字符串直接用
console.log(buf1)      // <Buffer 48 65 6c 6c 6f ...>

// ✅ 正确:先解码
console.log(buf1.toString())  // 'Hello, 世界'

Stream(流)

Stream 是 Node 处理流式数据的抽象,适合大文件、网络传输、压缩解压、视频、日志、代理转发。

四类流:

类型说明例子
Readable可读流fs.createReadStream、HTTP 请求体
Writable可写流fs.createWriteStream、HTTP 响应
Duplex可读可写TCP Socket
Transform转换流(Duplex 的特殊形式)zlib.createGzip()

为什么 Stream 比一次性读取更重要:

js
// ❌ 读超大文件:可能撑爆内存
const data = fs.readFileSync('huge-file.json')  // 全部读入内存

// ✅ 流式读取:边读边处理,内存可控
const readable = fs.createReadStream('huge-file.json')
readable.on('data', (chunk) => process(chunk))

背压(Backpressure)

背压就是:上游生产数据太快,下游消费不过来,中间缓冲区越堆越多。

常见场景:

  • 大文件读取 → 网络发送(读很快,写比较慢)
  • 数据库导出 → HTTP 下载
  • 上传流 → 压缩流 → 落盘

为什么背压重要: 没有流控,内存会不断上涨,严重时会把进程拖垮。

手动处理背压(了解原理):

js
const readable = fs.createReadStream('large.txt')
const writable = fs.createWriteStream('output.txt')

readable.on('data', (chunk) => {
  const canContinue = writable.write(chunk)
  if (!canContinue) {
    readable.pause()           // 下游满了,暂停上游
    writable.once('drain', () => {
      readable.resume()        // 下游排空了,恢复上游
    })
  }
})

pipeline()

推荐用 stream.pipeline() 而不是手搓多个 pipe()

为什么 a.pipe(b).pipe(c) 不够: 一旦中间某个流出错,错误与关闭逻辑容易遗漏,最终造成句柄泄漏、连接不释放、响应挂住。

js
import { pipeline } from 'stream/promises'
import { createReadStream, createWriteStream } from 'fs'
import { createGzip } from 'zlib'

// ✅ pipeline 会自动处理:错误传递、资源关闭、流结束
await pipeline(
  createReadStream('input.txt'),
  createGzip(),
  createWriteStream('output.txt.gz')
)
console.log('压缩完成')

pipeline() vs pipe()

对比pipe()pipeline()
错误处理需手动处理每个流自动传递
资源清理容易遗漏自动关闭
Promise✅(stream/promises
生产推荐不推荐✅ 推荐

Demo 里直接 pipe() 很常见,但线上链路更推荐 pipeline(),因为它更强调整条流管道的生命周期管理。


自定义 Transform 流

js
import { Transform } from 'stream'

class UpperCaseTransform extends Transform {
  _transform(chunk, encoding, callback) {
    const upper = chunk.toString().toUpperCase()
    this.push(upper)   // 推给下游
    callback()         // 告知上游可以继续发
  }
}

await pipeline(
  createReadStream('input.txt'),
  new UpperCaseTransform(),
  createWriteStream('output.txt')
)