好,我用大白话给你解释 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 的
req和res本身就是流) - 数据压缩/解压、加密/解密
- 任何需要边读边处理的场景
一句话总结:数据太大、太慢、太无限的时候,就把它变成流,一段一段地搞。
7. 和普通数组/字符串对比
| 方式 | 内存占用 | 处理速度 | 适用场景 |
|---|---|---|---|
一次性读完(比如 fs.readFileSync) | 高 | 快(对小文件) | 小文件、配置、JSON |
| 流式处理 | 低 | 可能稍慢一点(但不会卡死) | 大文件、实时数据、网络 |
选择原则:能装进内存的用普通方法,装不下的用流。
希望这样解释你明白了。流听起来很技术,其实就是“数据分块传输”的思想,就像水流一样自然。你只要记住 on('data') 和 pipe,就能应对 90% 的场景。