← 返回题目列表

Node.js Stream 是什么?有什么应用场景?

高频 中等 第 10 / 27 题 更新于 2026/07/27
Node.jsStream背压文件处理

简化版

Stream 是 Node 处理流式数据的抽象,可以边读边处理边写,避免一次性把大文件或大响应读进内存。常见类型有 Readable、Writable、Duplex、Transform,核心优势是低内存和背压控制。

详细版

示例:

import fs from 'node:fs';

fs.createReadStream('input.txt')
  .pipe(fs.createWriteStream('output.txt'));

Stream 类型:

  • Readable:可读流,如文件读取、HTTP 请求。
  • Writable:可写流,如文件写入、HTTP 响应。
  • Duplex:既可读又可写,如 TCP socket。
  • Transform:转换流,如 gzip 压缩。

pipe 会自动处理数据流动和背压,比手动监听 data 更安全。

完整版教学

一、为什么需要 Stream

如果读取一个 2GB 文件,用 readFile 会尝试把整个文件读进内存,容易造成内存暴涨。Stream 把数据切成 chunk,一块块处理。

这非常适合文件上传下载、日志处理、代理转发、压缩解压、音视频处理。

二、背压是什么

背压是生产速度大于消费速度时的调节机制。比如读文件很快,但网络写出很慢,如果不控制,内存里会堆积大量待写数据。

Stream 的 pipe 会在写入端压力大时暂停读取,等写入端 drain 后继续,从而保护内存。

三、Transform 流的价值

Transform 流可以边读边改:

source.pipe(gzip).pipe(destination);

数据不需要完整落地,就能完成压缩、加密、格式转换等处理。Transform 同时受上游输入和下游消费速度约束,自定义实现必须只在当前 chunk 处理完成后调用回调,错误则通过回调或销毁流传播。

四、面试追问与工程落地

常见追问是“data 事件和 pipe 有什么区别”。监听 data 会进入 flowing 模式,数据会持续推送,需要自己处理暂停和恢复;pipe 封装了很多背压逻辑,更适合普通管道场景。

工程中处理 Stream 要注意错误传播。多个流 pipe 时,任意一段出错都要处理。现代 Node 推荐 pipeline,它能统一处理完成和错误。

五、背压的状态机

手动向 Writable 写数据时,writable.write(chunk) 返回 false 表示内部缓冲已达到或超过 highWaterMark,生产者应停止继续写,等 drain 事件后恢复。highWaterMark 是开始施加背压的阈值,不是严格的内存上限;正在处理的 chunk、上下游缓冲和编码转换仍会占用额外内存。

for await (const chunk of source) {
  if (!destination.write(chunk)) {
    await once(destination, 'drain');
  }
}
destination.end();
流类型主要缓冲背压关注点
Readable等待消费的数据消费变慢时暂停拉取
Writable等待写出的数据write() 为 false 后等待 drain
Duplex读、写两套独立缓冲两端速度分别控制
Transform输入与输出两侧缓冲转换速度和下游速度都可能限流

处理 2GB 文件时,readFile 需要容纳完整内容;若显式以 64KiB chunk 流式处理,应用通常只保留有限数量的 chunk,内存量级从 GB 降到 MB 附近。实际峰值仍取决于并行管道、各级 highWaterMark、业务是否缓存 chunk,不能简单等同于 64KiB。

六、pipeline、错误传播与消息边界

现代 Node 推荐 Promise 版 pipeline 连接多段流,因为任意一段失败时它会传播错误并销毁相关流,调用方可以用一次 try/catch 处理完成或失败。普通的连续 .pipe() 不会自动把所有错误汇总到一个 Promise,忘记监听某一段就可能泄漏文件描述符或留下半截响应。

import { pipeline } from 'node:stream/promises';

await pipeline(source, transform, destination);

Readable 也可以使用 for await...of 消费,它按照消费者节奏拉取数据,适合需要逐块 await 业务逻辑的场景。objectModehighWaterMark 统计的是对象数量而不是字节数,一个对象可能很大,因此仍要限制单对象体积。

TCP、文件和普通 Stream 只保证字节顺序,不保证一次 chunk 就是一条完整业务消息。JSON 行、长度前缀协议或多字节 UTF-8 文本都可能跨 chunk,需要累积未完成部分再解析;不能对每个 chunk 直接 JSON.parse 或假定它一定以字符边界结束。

Stream 解决的是增量处理和流量调节,不会自动定义业务消息边界,也不会替你消除每一层的错误处理。

七、常见误区与追问

  • 误区:highWaterMark 是 Stream 绝不会超过的内存上限。 它只是触发背压的阈值,处理中数据、其他管道缓冲和业务引用都可能让内存更高。
  • 误区:调用 write() 返回 false 代表写入失败。 该 chunk 已被接收,只是生产者应暂停,等 drain 后再继续写。
  • 误区:一个 chunk 就对应发送方的一条消息。 chunk 边界由缓冲和调度决定,应用协议必须自行处理拆包与粘包。
  • 追问:为什么 pipeline 比连续 pipe 更稳妥? 它统一传播完成和失败,并在出错时清理相关流,减少遗漏错误监听和资源泄漏。
  • 追问:objectMode 的 highWaterMark 表示什么? 它按对象个数计数而非字节数,所以还要约束每个对象本身的大小。
  • 追问:什么时候适合 for await...of 需要逐块执行可等待的业务逻辑、显式控制流程时更直观,它仍会配合 Readable 的背压。
  • 追问:Stream 一定比一次性读取更快吗? 不一定;它主要降低峰值内存并改善首字节时间,小文件可能因管道和回调开销没有吞吐优势。

八、加强记忆

Stream 记成“受控的数据水管”:Readable 出水,Writable 接水,Transform 中途加工,write(false) 提醒上游等待 drain。大文件和网络传输用流,核心是增量处理与背压,而不是承诺固定内存或天然更快。多段管道优先使用 pipeline,同时牢记 chunk 不等于业务消息。