先记住这个答案
Readable 提供数据供消费者读取,Writable 接收写入数据,Duplex 同时具有可读与可写两侧,而 Transform 是一种特殊 Duplex,读出数据由写入数据经过转换产生。网络 socket 的收发可以相对独立,压缩流则体现输入到输出的转换关系。双工并不意味着两侧共享同一缓冲或同时结束;四种类型都要考虑错误、背压与资源生命周期,objectMode 只是改变传输单位,不是额外一种流方向。
- 可读与可写以流消费者的视角判断
- Transform 是具有转换关系的双工流
- 双工两侧的缓冲和完成状态要分别理解
可读和可写描述的是调用方能力
文件读取流向调用方提供文件内容,因此是 Readable;文件写入流接收调用方交来的内容,因此是 Writable。不要从磁盘或远端服务的视角反过来命名,否则分析 HTTP 请求和响应时很容易混淆方向。
Readable 可以通过异步迭代、pipe 或事件等方式消费,Writable 通过 write 和 end 接收数据。消费方式与流类型是不同维度,同一个可读流不因为选择 data 事件就变成另一种类型。
Duplex 与 Transform 的关键差别是数据关系
TCP socket 可以一边接收远端消息,一边发送本地消息,两边内容不必一一对应。Transform 则通过转换逻辑把写入输入变成读出结果,例如压缩数据、解析记录或过滤内容,因此适合放在处理链中间。
转换也不保证一个输入 chunk 对应一个输出 chunk。压缩或分帧可能缓存多个输入后输出一段,也可能把一段输入拆成多个结果;业务协议不能把 chunk 数量当作记录数量或请求数量。
双工两侧与对象模式需要独立预算
Duplex 和 Transform 的可读、可写两侧各有状态与缓冲。写入端完成不代表读出结果已被消费者拿完;如果输出无人消费,转换链也可能因背压停住,不能只观察输入已经写完。
objectMode 允许把非 null 的 JavaScript 值作为对象单位传递,并按对象数量管理相关阈值,但对象大小仍会影响内存。传输大对象并不会因为计数只有几个就变得便宜,生命周期和总量预算仍需设计。
容易答错的地方
- 认为 Duplex 的输出一定来自输入转换
- 这正是需要区分普通双工与转换流的原因。网络收发可以独立,而转换流定义了加工关系;选错抽象可能把协议通信硬写成数据转换,导致完成条件和错误处理难以表达。
- 把 objectMode 当作不会占内存的消息通道
- 对象模式改变计量单位,不会限制每个对象占用的内存,也不会自动复制或验证业务对象。应控制对象规模与队列长度,避免把整份大型数据塞进一个对象来绕过看起来很小的数量阈值。
面试官还会怎么问?
Transform 的 finish 和 end 为什么可能不同步?
finish 关注写入侧完成,end 关注可读侧已经被消费完。转换结果可能还在可读缓冲里,因此需要按照任务目标等待相应两侧,不能把写入侧完成直接当成整条数据链已经交付。
读取 HTTP 请求体与写响应属于什么方向?
在 Node 原生服务端,传入请求可作为可读来源消费请求体,响应对象则接收服务端写出的响应数据。实际网络连接本身具有双向能力,但请求和响应封装给调用方的接口职责不同。
为什么学习类型之后还要学习 pipeline?
类型只描述接口与方向,多个流连接之后还需要处理背压、结束、错误传播和清理。pipeline 提供整条链的管理能力,但外部文件或业务事务仍有各自责任,不能只会连接箭头就忽略失败路径。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。