先记住这个答案
read 返回 null 不一定表示流结束,也可能是当前缓冲没有可返回的数据,或尚未满足指定读取量。生产方 push(null) 表示不再提供新数据,但已有缓冲仍需被消费者读完;end 事件表示可读数据已经耗尽,readableEnded 在 end 发出后为 true。手动读取时应在 readable 通知中读取到 null 后停止本轮循环,再等待后续通知或结束。不要在一个同步循环里反复空读,也不要第一次读到 null 就截断任务。
- read 返回 null 不是单独的 EOF 证明
- push(null) 后仍可能有缓冲数据
- end 与 readableEnded 表示可读侧完成
暂时无数据时应把控制权交回事件循环
对于异步数据源,首次 read 可能发生在网络或文件数据准备好之前。返回 null 后继续紧密同步轮询,只会占用当前线程,使真正负责提供数据的回调更难运行,不会让数据更快到达。
如果显式指定读取大小,字节流可能等待足够缓冲才返回,接近结束时又有剩余数据处理规则。应按流模式和读取协议设计,不要把每次 null 都解释成“对方已经关闭连接”。
用同一个来源观察前后状态
下面先对空来源读取,再推入一个对象模式值和结束标记。消费者仍能取出那个值,随后等待 end,才能观察到 readableEnded 已为 true;这把无数据、生产结束与消费结束分开了。
end 需要数据被实际消费,不能只给来源 push(null) 就假设监听器已经执行。这个示例没有外部资源,真实来源还需处理 error 和提前关闭,并在不再需要数据时明确取消或销毁。
import { Readable } from 'node:stream';
export async function inspectReadStates() {
const source = new Readable({ objectMode: true, read() {} });
const before = source.read();
const beforeEnded = source.readableEnded;
const done = new Promise<void>((resolve, reject) => {
source.once('end', resolve);
source.once('error', reject);
});
source.push('later');
source.push(null);
const value = source.read();
source.read();
await done;
return { before, beforeEnded, value, afterEnded: source.readableEnded };
}初次读取为 null 时 beforeEnded 仍为 false,随后可以读到 later,消费完成后 afterEnded 才为 true。代码中的空 read 实现仅用于主动推送的受控示例,不代表真实数据源可以省略生产逻辑。
完成状态还要与错误路径一起判断
如果来源因错误被 destroy,正常 end 可能不会到达。等待完整任务时可以使用 finished 或 pipeline 等工具,并根据所需读写侧处理异常关闭;单独监听一个成功结束事件容易让 Promise 永远不结算。
对象模式中的 null 仍被保留为结束标记,不能作为普通业务对象传输。业务协议若需要表达空值,可以使用包装对象,例如带有明确字段的记录,避免把空业务值和流结束信号混淆。
容易答错的地方
- 第一次 null 就返回已完成
- 这会把异步来源暂时没准备好的情况误判成结束,丢失后续到达的数据。应等待 readable 或完成事件,并保留错误处理,不能用一次读取结果替代整个来源的生命周期状态。
- push(null) 后立即断言 end 已触发
- 生产方声明结束不代表缓冲已被消费者拿完,也不代表事件已经在当前调用栈里发出。应通过实际消费和完成等待验证,尤其不要在测试里用同步断言掩盖异步结束过程。
面试官还会怎么问?
readableEnded 为 true 能证明外部资源都释放了吗?
它说明可读侧已经发出 end,不代表所有外部资源或双工写入侧都已结束。需要根据流类型和任务所有权检查关闭与清理,不能把可读完成扩展成整个系统资源完成。
对象模式可以 push(null) 传空值吗?
不可以,null 仍具有结束语义。需要空业务值时应使用包装记录或其他明确表示,让消费者能区分正常数据与生产结束,避免数据协议意外终止流。
为什么不建议用定时器不停检查 read?
可读流已经提供通知机制,轮询会增加延迟和无效唤醒,还容易漏掉错误或结束状态。应采用 readable 配合 read、异步迭代或管道等合适风格,并为取消和失败提供统一路径。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。