Node.js 的“流”怎么理解

4 阅读3分钟

好,我用大白话给你解释 Node.js 里的“流”。


1. 流是什么?

流就是“一段一段地传输数据”,而不是一次性把所有数据都装进内存。

举个例子:

  • 不用的方式:你喝一整桶水,先把桶里的水全部倒进你的肚子里(内存),再开始消化 → 如果桶太大,你肚子会撑爆(内存溢出)。
  • 流的方式:你拿一根吸管,一边吸一边喝,水从桶里经过吸管源源不断进到你嘴里,你随时都在喝,但肚子里不会一下子装太多。

在 Node.js 里,读取大文件、网络传输、处理视频等场景,数据是一块一块(chunk)来的,就可以用流处理。


2. 为什么需要流?

假设你要读取一个 10GB 的日志文件:

  • 不用流:fs.readFileSync 会把 10GB 一次性读到内存 → 内存爆炸,程序卡死。
  • 用流:fs.createReadStream 每次读 64KB(默认),处理完这 64KB 再读下一块 → 内存占用很小。

核心好处:节省内存,提高效率,能处理无限大的数据。


3. 流的四种“角色”

Node 里有四种流,名字很吓人,但意思很简单:

类型大白话解释比喻
可读流只能从里面读取数据水龙头出水(你只能接水)
可写流只能往里面写入数据排水管(你只能往里倒水)
双工流既能读又能写,但读和写互相独立电话(你既能说话也能听,但两边互不干扰)
转换流也是既能读又能写,但写入的数据会经过转换后再读出榨汁机(你塞进去水果,它吐出果汁)

常见例子:

  • fs.createReadStream() → 可读流
  • fs.createWriteStream() → 可写流
  • net.Socket(TCP 连接)→ 双工流
  • zlib.createGzip()(压缩)→ 转换流

4. 怎么用流?几个事件就够了

从可读流读数据

const readStream = fs.createReadStream('bigfile.txt');
readStream.on('data', (chunk) => {
  console.log(`收到一块数据,大小 ${chunk.length} 字节`);
  // 处理这块数据
});
readStream.on('end', () => {
  console.log('数据读完了');
});

就像水龙头出水,来一块(data 事件)你处理一块,最后关水(end 事件)。

往可写流写数据

const writeStream = fs.createWriteStream('output.txt');
writeStream.write('第一块数据\n');
writeStream.write('第二块数据\n');
writeStream.end();  // 告诉它写完了

像往水管倒水,倒一瓢(write),最后倒完(end)。

用管道(pipe)连接两个流

pipe 是流的终极武器,直接把可读流接到可写流上,数据自动流过去。

const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('dest.txt');
readStream.pipe(writeStream);
// 数据就像水从 source 流到 dest,自动处理速度匹配

你完全不用管 data 事件了,一行代码搞定文件复制。


5. 背压(backpressure)—— 流的“堵车处理”

大白话: 如果读得太快,写得太慢,数据会堆积在内存里。流会自动让读的慢一点,等写的跟上了再继续读。

就像倒啤酒:你倒太快(读),杯子来不及喝(写),就会溢出来。流内部会自动“等一下,别倒了”,避免内存爆炸。你一般不用管,pipe 方法会自动处理背压。


6. 什么时候用流?

  • 处理大文件(几百 MB 以上的日志、视频、数据库备份)
  • 网络请求/响应(HTTP 的 reqres 本身就是流)
  • 数据压缩/解压、加密/解密
  • 任何需要边读边处理的场景

一句话总结:数据太大、太慢、太无限的时候,就把它变成流,一段一段地搞。


7. 和普通数组/字符串对比

方式内存占用处理速度适用场景
一次性读完(比如 fs.readFileSync快(对小文件)小文件、配置、JSON
流式处理可能稍慢一点(但不会卡死)大文件、实时数据、网络

选择原则:能装进内存的用普通方法,装不下的用流。


希望这样解释你明白了。流听起来很技术,其实就是“数据分块传输”的思想,就像水流一样自然。你只要记住 on('data')pipe,就能应对 90% 的场景。