Stream 基础:四种流与 pipe 管道
本节目标
- 理解"流(Stream)"解决什么问题:让数据像水流一样"边到边处理",不必等全部到齐
- 掌握四种流(Readable / Writable / Duplex / Transform)的区别与典型用途
- 用
pipe把流串起来,并手写第一个Transform转换流
为什么需要流? 假设你要把一个 2GB 文件从 A 复制到 B。如果一次性 readFile 读进内存,内存直接爆掉;如果用流,数据像水流:源头一块块流出,中途可以边流边处理,终点一块块接收——全程内存只保留"当前这一小块"。这就是流的核心价值:处理的是"流动的数据",而不是"完整的数据集"。
四种流:
Readable(可读流):数据的源头,如fs.createReadStream、HTTP 请求体。Writable(可写流):数据的终点,如fs.createWriteStream、HTTP 响应。Duplex(双工流):既能读又能写,如 TCP socket、WebSocket。Transform(转换流):边读边写、对数据做变换,如zlib.createGzip、加密流。
// 运行环境:Node.js 14+(无需联网/依赖)
// 保存为 buf-l2.js,执行:node buf-l2.js
const { Transform, Readable, Writable } = require('stream')
// 1) 一个最简单的"转换流":把读到的每一块都转成大写
class Upper extends Transform {
// _transform(chunk, encoding, callback):拿到一块数据,处理后用 callback(null, 新块) 交出去
_transform(chunk, encoding, callback) {
const up = chunk.toString().toUpperCase()
callback(null, Buffer.from(up))
}
}
const upper = new Upper()
upper.on('data', (c) => process.stdout.write('[流出] ' + c)) // 'data' 事件:有块流出
upper.on('end', () => console.log('\n[结束] 流已读完'))
upper.write('hello ') // 写入数据(会触发 _transform)
upper.write('stream\n')
upper.end() // 声明"写完了"
// 2) 用 pipe 把"可读流"自动接到"可写流"(无需手动监听 data 事件)
const src = Readable.from(['行一\n', '行二\n', '行三\n']) // 从数组造一个可读流
const out = new Writable({
write(chunk, enc, cb) { process.stdout.write('>> ' + chunk); cb() } // cb() 表示这块写完了
})
src.pipe(out) // 自动把 src 的数据块"搬运"给 out,直到结束背压(backpressure)是什么? 想象水龙头(源头)出水太快,而下水管(终点)处理太慢,水就会溢出来。流里同理:源头产数据比终点消费快时,多余数据会堆在内存。pipe 内置了背压机制——终点忙不过来时会通知源头"暂停",等空了再"继续",从而内存稳定。所以优先用 pipe / pipeline,不要手动 'data' 狂读。
名词解释
- 流(Stream):一组有序、可逐步获取的数据。它把"数据生产"和"数据消费"解耦,让你可以"边产生边处理",而不是等全部就绪。
- 背压(Backpressure):当数据生产速度 > 消费速度时,系统反向通知生产者"慢一点"的机制。它是流能稳定处理大数据的根本保证。
- chunk(数据块):流在任意时刻传输的"一小段"数据,通常是
Buffer。你处理的是一个个 chunk,而不是整份文件。 - 双工流(Duplex):同时具备"可读"和"可写"能力的流,如一个 TCP 连接:你既能
write发数据,也能on('data')收数据。 - 转换流(Transform):一种特殊的双工流,读入一块数据、做变换、再输出一块。压缩、加密、转码都是它的典型应用。
课后练习
- 什么场景必须上流,而不是
readFile/readFileSync?- 答案:数据量大到放不进内存(大文件复制、日志分析、视频转码),或数据是"持续产生"的(网络请求、实时日志)。流能保持内存恒定。
- 手写
Transform时,callback(err, data)的两个参数分别是什么?- 答案:第一个是错误(正常传
null),第二个是"这一块处理完要交出去的数据"。忘了调callback会导致流"卡死"不再处理后续块。
- 答案:第一个是错误(正常传
- 为什么说
pipe自动处理了背压,而你手动on('data')不一定?- 答案:
pipe内部实现了暂停/恢复逻辑:终点write返回false(忙)时,源头会被pause(),等'drain'再resume()。手动监听data若不自己处理"返回值 false 就暂停",就会无限堆积数据撑爆内存。
- 答案:
总结
流是 Node 处理"大"与"持续"数据的统一范式,它的思想一句话概括就是:别等全部到齐,来一块处理一块。我刻意把"背压"放在这一节强调,是因为它是流的灵魂——很多人第一次手写流时,只记得 pipe 很方便,却不知道底层那套"源头太快→终点喊停→源头暂停→终点恢复"的节奏控制才是它稳如泰山的真正原因。你今天写的 Upper 转换流,和 Node 内置的 zlib.createGzip、TLS 加密流是同一个祖宗(都继承 Transform)。理解了这一点,下一节的"文件压缩管道""网络分包"就不再是新概念,而只是"把不同的 Transform 串到 pipe 上"而已。请记住:凡是看到"实时""大文件""边传边处理"这几个词,第一反应就应该是——上流。