先记住这个答案
end 表示可读侧数据已经被消费完,finish 表示可写侧在 end 后完成了相应写入处理,close 表示流及底层资源进入关闭状态,可能来自正常结束也可能来自异常销毁。stream.finished 提供对相关完成与错误路径的统一观察,Promise 版本可 await,但不会替你消费可读数据。双工流需要明确等待哪一侧,必要时通过选项限定范围。正常流完成也不自动等于远端业务确认或可靠落盘,资源与业务协议仍有自己的保证。
- end 看可读侧,finish 看可写侧
- close 可能来自正常或异常结束
- finished 观察完成,但不会自动消费数据
三个事件不是可以互换的结束别名
只监听 Readable 的 end,可能漏掉错误或提前关闭;只监听 Writable 的 close,则可能把异常销毁误判为成功。应同时理解该流的方向和失败路径,选择能收敛正常与异常结束的方式。
finished 的 Promise 接口便于将结果纳入 async/await,但它只观察状态,不会主动 read 或把数据写到目的地。对还有未消费输出的双工流,默认等待可能一直持续,这往往是消费责任没有完成。
双工写入结束后仍可能留有输出
下面故意只等待 PassThrough 的可写侧,随后检查可读侧尚未结束并取出数据。之后再消费剩余可读状态并等待整个流,展示两个方向的完成可以分开观察。
这里的 readable:false 表示调用者只想观察可写侧,不表示销毁可读侧或丢弃输出。只有业务确实只需要该侧结果时才缩小等待范围,否则为了让 Promise 尽快返回而忽略另一侧,会留下未消费数据。
import { PassThrough } from 'node:stream';
import { finished } from 'node:stream/promises';
export async function inspectDuplexCompletion() {
const stream = new PassThrough({ objectMode: true });
const writableDone = finished(stream, { readable: false, cleanup: true });
stream.end('A');
await writableDone;
const stillReadable = !stream.readableEnded;
const value = stream.read();
const allDone = finished(stream, { cleanup: true });
stream.resume();
await allDone;
return { stillReadable, value, readableEnded: stream.readableEnded };
}测试应先观察到写入已完成但可读侧仍有 A,随后消费后 readableEnded 为 true。示例使用受控内存流,不能由此推断文件落盘或网络接收端已经确认业务处理。
观察器本身也要有清理责任
Promise 版本 finished 可以通过 cleanup 选项在完成后移除它添加的监听器;回调版本有对应清理函数。默认保留部分监听器与处理不规范流的迟到错误有关,长期重复观察时应理解这一选择。
完成监听器清理与销毁资源不是同一个动作。调用者仍要管理自己注册的其他监听器、外部句柄和取消信号;不要以为一个 Promise 已经结束,就自动清理了整个业务函数创建的所有对象。
容易答错的地方
- 只等 end 就认定所有资源已经释放
- end 仅说明可读数据耗尽,不覆盖双工写入侧和任意外部资源。应根据流实现和任务目标检查关闭与清理,尤其要处理失败时可能没有正常 end 的情况。
- 为了不等待而随意关闭 readable 检查
- 这只是缩小了完成条件,并没有消费或释放另一侧。若业务仍需要全部输出,应该补上真实消费路径;不能把忽略某侧当成解决悬挂任务的通用办法。
面试官还会怎么问?
finish 能证明文件已经可靠落盘吗?
它表示可写流按自身实现完成了写入流程,不自动提供更强的存储持久化保证。需要应对掉电等场景时应了解底层文件同步与系统语义,不能把流事件扩大成业务所需的全部可靠性。
为什么 finished 有时报告提前关闭错误?
流在满足预期的正常完成条件前关闭,可能意味着来源中断或下游提前销毁。应检查真正的取消与错误路径,确认是否允许这种短路,不能简单吞掉错误并把残缺数据当完整结果。
pipeline 和 finished 应该怎么分工?
pipeline 用于连接并管理整条数据处理链,finished 更适合观察一个已有流的完成。选择取决于谁拥有连接、消费和取消责任,不能把只观察状态的工具当作自动传输数据的工具。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。