互联网技术 / 互联网资讯 · 2024年1月9日

如何理解 Node.js 中的 Stream 及其适用场景

在 Node.js 中,Stream(流)是一种非常重要的数据处理机制,常用于按顺序传输和处理数据。它的核心特点是:不会一次性把全部内容加载到内存中,而是将数据分成一小块一小块地读取、处理或写入。这种方式特别适合处理大文件、网络请求以及各种 I/O 操作。

很多时候,流可以理解为一串连续传递的数据,最常见的是字节流。当然,除了字节流之外,也可以扩展到音频流、视频流、数据流等不同形式。

与传统一次性读取完整文件的方式相比,流的优势在于节省内存、提高处理效率,并且更适合持续性的数据传输场景。

从结构上看,流通常包含三个部分:source、pipe 和 dest。数据从 source 出发,通过 pipe 管道传输,最终到达 dest。常见写法如下:

source.pipe(dest)

通过这种方式,可以很方便地把读取到的数据直接传递给目标,而不需要开发者手动管理每一段数据。

流的类型

在 Node.js 中,流大致可以分为四类,这些类型在文件系统、网络通信和 HTTP 模块中都非常常见。

  • 可读流(Readable):用于读取数据。例如 fs.createReadStream() 可以按块读取文件内容。

  • 可写流(Writable):用于写入数据。例如 fs.createWriteStream() 可以将数据持续写入文件。

  • 双工流(Duplex):既可以读取数据,也可以写入数据。例如 net.Socket,它支持双向通信。

  • 转换流(Transform):属于一种特殊的双工流,既能读写数据,又能在传输过程中对数据进行处理或转换。例如压缩、解压、编码转换等场景都会用到转换流。

其中,可读流和可写流属于单向操作,理解起来比较直接;而双工流和转换流则支持双向处理,能力更强,也更适合复杂场景。

Node.js 中的常见体现

Node.js 对流的支持非常广泛。比如在 HTTP 模块中,请求对象 Request 本质上是一个可读流,而响应对象 Response 则是一个可写流。文件系统模块 fs 也提供了大量基于流的 API,用于高效处理文件读写。

双工流的理解

双工流可以同时进行读取和写入,但这两个方向通常是相互独立的。可以把它类比为全双工通信,例如 WebSocket:发送和接收可以同时进行,彼此不会互相阻塞。

因此,在一些需要双向实时通信的场景中,双工流会非常有价值。

转换流的理解

转换流是在数据流动过程中对内容进行加工的一种机制。输入一份数据,输出的可能是经过处理后的另一份数据。

一个典型例子是 Babel 转码:左侧输入 ES6 代码,经过转换流处理后,右侧输出 ES5 代码。类似地,文件压缩、解压、加密、解密等流程,本质上也都可以通过转换流完成。

适用场景

Stream 最常见的应用场景,本质上都集中在 I/O 操作上。HTTP 请求、文件读写、网络传输等都属于典型的 I/O 场景。

如果一次性处理的数据量很大,那么直接整体读入内存往往会带来较高的性能开销。使用流之后,可以把大任务拆分成连续的小块处理,就像水在管道中不断流动一样,直到整个过程结束。这种模式更高效,也更稳定。

常见使用场景包括以下几类:

  • 向客户端返回文件内容

  • 文件复制、读写等文件操作

  • 构建工具和打包工具的底层处理

向客户端返回文件

在处理 HTTP GET 请求时,如果需要把一个文件返回给客户端,使用流是一种很合适的方式。因为响应对象本身就是可写流,所以可以直接将文件可读流通过 pipe() 传递给响应对象,把文件内容边读边发给客户端。

文件操作

在文件复制或大文件处理时,通常会创建一个可读流和一个可写流,然后通过管道将两者连接起来。这样不需要一次性把完整文件加载进内存,就能完成数据转移。

构建工具的底层能力

很多前端工程化工具本质上都依赖 Node.js,而打包、编译、压缩这些过程都离不开频繁的文件操作,因此也会大量使用流。例如一些自动化构建工具在处理文件管道时,就会充分利用 Stream 的能力来提高效率。

总体来看,Stream 是 Node.js 中非常核心的一种机制。它不仅提高了 I/O 处理效率,还能有效降低内存占用,尤其适合文件处理、网络传输和数据转换等需要持续读写的场景。理解流的工作方式和分类,对于掌握 Node.js 的高性能处理模式非常重要。