Node.js Readable可读流完全指南:streamify-your-node-program教你用3行代码读懂_read、push与end事件
【免费下载链接】streamify-your-node-program对Node.js中 stream模块的学习积累和理解项目地址: https://gitcode.com/gh_mirrors/st/streamify-your-node-program
Node.js Readable 可读流完全指南:streamify-your-node-program 教你用 3 行代码读懂 _read、push 与 end 事件
如果你正在学习Node.js 流(Stream),大概率卡在 Readable 可读流的三个概念上:_read方法、push方法和end事件。本文基于开源项目streamify-your-node-program(一个专注 Node.js stream 模块学习积累的教程项目)的文档与示例,用最小代码带你一次读懂可读流的工作原理——从数据怎么"泵"出来,到流如何正确结束。
什么是 Readable 可读流?
一句话定义:Readable 可读流作为"上游",负责产生数据并推送给下游,典型代表就是fs.createReadStream读文件流。
把它想象成一根水管:
- 🚰
push()—— 往水管里灌水 - 🔄
_read()—— 水泵的开关,下游需要水时自动开启 - 🏁
push(null)—— 宣布水源枯竭,水管不会再有水了 - 📢
end事件 —— 下游把最后一滴水也取走时的信号
项目中的 docs/readable.md 对这部分有完整讲解,配合 example/principle/ 目录下的十余个最小示例,是理解可读流原理的绝佳材料。
核心 API 速览:谁是谁的"水泵"
| API | 谁来写 | 作用 |
|---|---|---|
_read() | 你 | 数据产生的入口,Node 在需要时自动调用它 |
push(chunk) | 你 | 把数据送入可读流内部缓存 |
push(null) | 你 | 声明数据已全部产生,结束流 |
data事件 | Node 触发 | flowing 模式下每收到一块数据触发一次 |
end事件 | Node 触发 | 数据被完全消耗时触发 |
readable事件 | Node 触发 | 缓存有新数据或流到达尽头时触发 |
💡 记忆口诀:你只写_read和push,剩下的事件全交给 Node。
3 行核心代码:最小可读流长这样
创建一个可读流的全部"生产逻辑"只有三行(完整代码见 example/principle/_read.js):
const Readable = require('stream').Readable const dataSource = ['a', 'b', 'c'] const readable = Readable() readable._read = function () { if (dataSource.length) { this.push(dataSource.shift()) // 第 1 步:从数据源取一块 } else { this.push(null) // 第 2 步:取完了,宣告结束 } } readable.on('data', data => process.stdout.write(data)) // 第 3 步:监听 data 消费数据 // 输出:abc这三行就是所有可读流的骨架:_read是水泵,push是灌水,push(null)是关闸。
关于 push:同步、异步都行
push既可以在_read里同步调用,也可以异步调用(真实场景大多如此,比如读文件、请求网络)。example/principle/push.js 展示了用process.nextTick异步 push 的写法,效果完全一致——数据总是要等下游来"拉"才会真正流出。
_read 是如何被触发的?
新手最大的疑惑:_read什么时候被调用?答案是——当下游需要数据、而缓存不足时,Node 内部自动调用_read,你不需要(也不应该)手动调用它。
流程是这样的:
- 下游监听了
data事件(或pipe了另一个流) - Node 发现缓存没数据,自动调用你的
_read - 你的
_read里push入数据 - 数据从缓存流出,触发
data事件 - 缓存又空了 → 回到第 2 步,循环往复
⚠️_read只负责"补水",它不代表"把数据读完"。缓存水位(由highWaterMark控制,默认 16KB)决定每次补多少。相关原理可参考项目中的 docs/highWaterMark.md。
end 事件:最容易踩坑的"结束信号"
end事件 ≠ 调用push(null)的那一刻。它的触发必须同时满足两个条件:
- ✅ 已调用
push(null),声明不会再有新数据 - ✅ 缓存中残留的数据也被全部读取完
看 example/event-end.js 的真实输出顺序:
push a push b data <Buffer 61> push c data <Buffer 62> push null ← 这里还没 end!缓存里还有 'c' data <Buffer 63> end ← 数据真正消耗完,end 才触发💡 这说明end是给下游的"可以收尾了"信号;而忘了调用push(null)的流,会让下游永远等待——这是可读流头号 bug。
flowing 与 paused:两种消费模式
flowing 模式(默认推荐)🌊
只要监听了data事件,或执行了pipe,流就进入 flowing 模式,数据会持续不断地自动流出。90% 的场景你都在用它:
readable.on('data', data => console.log(data))完整示例见 example/flowing-mode.js。实际开发中更推荐readable.pipe(writable)的方式消费流,它还能自动处理背压,详见 docs/pipe.md。
paused 模式(主动控制)⏸️
流创建后的初始状态就是 paused 模式。此时数据不会自动流出,需要监听readable事件后手动调用read()从缓存取数:
readable.on('readable', function () { var data while (data = this.read()) { console.log(data) } }完整示例见 example/paused-mode.js。注意输出里<Buffer 61 62>是两块数据被合并读取的结果——read()不传参数时会一次性把缓存读空。
新手避坑清单 🛠️
- 忘了
push(null)→ 下游end永远不触发,流一直挂着 _read里无限同步 push 同一数据→ 缓存无限膨胀,内存爆炸。数据源耗尽后一定要push(null)- 空字符串不是结束→ 非 objectMode 下
push('')会被忽略;结束只能用push(null),见 example/principle/push-empty.js end时机理解错误→ 它发生在"消耗完"而非"产生完",排查流卡住时先确认这个- 在 flowing 模式下手动 read() 混用→ 会改变流的内部状态,非必要别混着来
小结:一张图读懂可读流生命周期
下游监听 data / pipe ↓ Node 自动调用 _read() ↓ push(chunk) 入缓存 ↓ data 事件流出数据 ↓ 数据源耗尽 → push(null) ↓ 缓存读空 → end 事件 🎉核心结论:
- 用
push产生数据,_read是自动触发的"水泵" - 必须用
push(null)结束流,否则下游一直等待 end事件 = 数据被完全消耗完的时刻- 绝大多数场景使用 flowing 模式 +
pipe消费即可
想继续深入?streamify-your-node-program 项目里还有 Writable 可写流(docs/writable.md)、objectMode(docs/objectMode.md)、Transform 流(docs/duplex-and-transform.md)以及 Browserify、Gulp 的流实战(docs/gulp.md),照着读下来,Node.js 流模块就彻底通了。
【免费下载链接】streamify-your-node-program对Node.js中 stream模块的学习积累和理解项目地址: https://gitcode.com/gh_mirrors/st/streamify-your-node-program
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考