流 API 概念

流 API 为 Web 平台添加了一组非常有用的工具,提供了允许 JavaScript 以编程方式访问通过网络接收的数据流并根据开发人员的需要对其进行处理的对象。与流相关的一些概念和术语可能对您来说是新的——本文解释了您需要了解的所有内容。

可读流

可读流是数据源,在 JavaScript 中由一个 ReadableStream 对象表示,该对象从一个**底层源**流出——这是网络上或您域上的其他某个地方的资源,您希望从中获取数据。

有两种类型的底层源

  • **推送源**在您访问它们时会不断地向您推送数据,并且由您决定开始、暂停或取消对流的访问。例如视频流和 TCP/Web 套接字
  • **拉取源**要求您在连接后显式地向其请求数据。例如,通过 fetch() 请求进行的文件访问操作。

数据按顺序以称为**块**的小片段读取。一个块可以是一个字节,也可以是更大的东西,例如特定大小的 类型化数组。单个流可以包含不同大小和类型的块。

Readable streams data flow

放置在流中的块被称为**入队**——这意味着它们正在队列中等待读取。一个**内部队列**跟踪尚未读取的块(请参阅下面的“内部队列和排队策略”部分)。

读取器、使用者和控制器

流中的块由**读取器**读取——它一次处理一个块的数据,允许您对数据执行任何您想要的操作。读取器加上与之相关的其他处理代码称为**使用者**。

还有一个您将使用的构造称为**控制器**——每个读取器都有一个关联的控制器,允许您控制流(例如,如果需要,可以关闭它)。

锁定

一次只能有一个读取器读取一个流;当创建读取器并开始读取流(**活动读取器**)时,我们说它被**锁定**到该流。如果您希望另一个读取器开始读取您的流,则通常需要在执行任何其他操作之前取消第一个读取器(尽管您可以**分流**流,请参阅下面的“分流”部分)

可读流和字节流

请注意,有两种不同类型的可读流。除了传统的可读流之外,还有一种称为字节流的类型——这是用于读取底层字节源的传统流的扩展版本。与传统可读流相比,允许 BYOB 读取器(BYOB,“自带缓冲区”)读取字节流。这种读取器允许将流直接读取到开发人员提供的缓冲区中,从而最大限度地减少所需的复制操作。您的代码将使用哪个底层流(以及扩展的读取器和控制器)取决于流最初是如何创建的(请参阅 ReadableStream() 构造函数页面)。

您可以通过诸如 Response.body(来自 fetch 请求)之类的机制使用现成的可读流,或者使用 ReadableStream() 构造函数创建您自己的流。

分流

即使一次只能有一个读取器读取流,也可以将流拆分为两个相同的副本,然后两个单独的读取器可以读取这两个副本。这称为**分流**。

在 JavaScript 中,这是通过 ReadableStream.tee() 方法实现的——它输出一个包含原始可读流的两个相同副本的数组,然后两个单独的读取器可以独立读取这两个副本。

例如,您可能在 ServiceWorker 中执行此操作,如果您想从服务器获取响应并将其流式传输到浏览器,但也要将其流式传输到 ServiceWorker 缓存。由于响应主体不能被使用多次,并且流一次只能被一个读取器读取,因此您需要两个副本才能执行此操作。

Teeing data flow

可写流

**可写流**是您可以写入数据的目标,在 JavaScript 中由 WritableStream 对象表示。它充当**底层接收器**的抽象层——一个写入原始数据的较低级别的 I/O 接收器。

数据通过**写入器**一次写入一个块到流中。与读取器中的块一样,一个块可以采用多种形式。您可以使用您喜欢的任何代码生成准备写入的块;写入器加上关联的代码称为**生产者**。

当创建写入器并开始写入流(**活动写入器**)时,据说它被**锁定**到该流。一次只能有一个写入器写入一个可写流。如果您希望另一个写入器开始写入您的流,则通常需要在将另一个写入器附加到它之前中止它。

一个**内部队列**跟踪已写入流但尚未由底层接收器处理的块。

还有一个您将使用的构造称为控制器——每个写入器都有一个关联的控制器,允许您控制流(例如,如果需要,可以中止它)。

Writable streams data flow

您可以使用 WritableStream() 构造函数使用可写流。目前,这些在浏览器中的可用性非常有限。

管道链

流 API 使得能够使用称为**管道链**的结构将流彼此连接起来。有两种方法可以促进这一点

  • ReadableStream.pipeThrough() — 将流通过**转换流**传递,可能在此过程中转换数据格式。例如,这可能用于编码或解码视频帧、压缩或解压缩数据,或以其他方式将数据从一种形式转换为另一种形式。转换流由一对流组成:一个从中读取数据的可读流和一个向其中写入数据的可写流,以及确保一旦写入数据即可读取新数据的适当机制。 TransformStream 是转换流的具体实现,但任何具有相同可读流和可写流属性的对象都可以传递给 pipeThrough()
  • ReadableStream.pipeTo() — 传递到充当管道链端点的可写流。

管道链的起点称为**原始源**,终点称为**最终接收器**。

pipe chain diagram

背压

流中的一个重要概念是**背压**——这是单个流或管道链调节读取/写入速度的过程。当链中后一个流仍然很忙并且尚未准备好接受更多块时,它会向后发送信号,通过链告诉前面的转换流(或原始源)降低传递速度,这样您就不会在任何地方遇到瓶颈。

要在 ReadableStream 中使用背压,我们可以通过查询控制器上的 ReadableStreamDefaultController.desiredSize 属性来询问使用者所需的块大小。如果它太低,我们的 ReadableStream 可以告诉其底层源停止发送数据,并且我们沿着流链回压。

如果稍后使用者再次想要接收数据,我们可以使用流创建中的拉取方法来告诉我们的底层源用数据馈送我们的流。

内部队列和排队策略

如前所述,流中尚未处理和完成的块由内部队列跟踪。

  • 对于可读流,这些是已入队但尚未读取的块
  • 对于可写流,这些是已写入但尚未由底层接收器处理的块。

内部队列采用**排队策略**,该策略指示如何根据**内部队列状态**发出背压信号。

通常,该策略将队列中块的大小与称为**高水位线**的值进行比较,该值是队列首选管理的最大总块大小。

执行的计算是

高水位标记 - 队列中块的总大小 = 期望大小

**期望大小**是指流为了保持持续流动但在大小上低于高水位标记,还可以接受的块的数量。块生成将根据需要进行减速/加速,以保持流尽可能快地流动,同时使期望大小保持在零以上。如果该值降至零(或以下),则表示块的生成速度超过了流的处理能力,这可能会导致问题。

例如,假设块大小为 1,高水位标记为 3。这意味着在达到高水位标记并应用背压之前,最多可以将 3 个块入队。