流–权威指南

2021-02-21 09:12:20

Streams API允许您以编程方式访问通过网络接收的数据流或通过任何本地方式创建的数据流,并使用JavaScript处理它们。流式处理涉及将要接收,发送或转换成小块的资源分解,然后一点一点地处理这些块。尽管流式传输是浏览器在接收要显示在网页上的HTML或视频之类的资产时的一种方式,但在2015年引入流式获取之前,JavaScript从未使用过此功能。

以前,如果您想处理某种资源(例如视频或文本文件等),则必须下载整个文件,等待将其反序列化为合适的格式,然后再处理它。随着流可用于JavaScript,这一切都会改变。现在,您可以在客户端上使用原始数据后逐步使用JavaScript逐步处理原始数据,而无需生成缓冲区,字符串或Blob。这解锁了许多用例,我在下面列出了其中的一些:

视频效果:通过可实时应用效果的变换流来传递可读视频流。

图像解码:通过将字节解码为位图数据的转换流,然后通过将位图转换为PNG的另一个转换流,将HTTP响应流传递给管道。如果安装在服务工作者的获取处理程序中,则可以透明地填充新图像格式(如AVIF)。

在详细介绍各种类型的流之前,让我介绍一些核心概念。

块是写入流或从流读取的单个数据。它可以是任何类型。流甚至可以包含不同类型的块。在大多数情况下,对于给定的流,块将不是最原子的数据单元。例如,字节流可能包含由16个KiB Uint8Array单元组成的块,而不是单个字节。

可读流表示您可以读取的数据源。换句话说,数据来自可读流。具体而言,可读流是ReadableStream类的实例。

可写流表示可以写入的数据的目标。换句话说,数据进入可写流。具体而言,可写流是WritableStream类的实例。

转换流由一对流组成:一个可写流(称为其可写面)和一个可读流(称为其可读面)。现实世界中的隐喻是即时翻译,他可以将一种语言即时翻译成另一种语言。以特定于变换流的方式,写入可写侧导致新数据可用于从可读侧读取。具体而言,任何具有可写属性和可读属性的对象都可以用作转换流。但是,标准的TransformStream类使创建正确纠缠的一对更加容易。

流主要通过将它们彼此管道连接来使用。可以使用可读流的pipeTo()方法将可读流直接传递给可写流,也可以使用可读流的pipeThrough()方法首先将可读流传递给一个或多个转换流。 。以这种方式管道连接在一起的一组流称为管道链。

一旦构建了管道链,它将传播有关块应流过的速度的信号。如果链中的任何一步仍不能接受块,它将通过管道链向后传播信号,直到最终告知原始源停止如此快地生成块。标准化流量的过程称为背压。

可以使用tee()方法来处理可读流(以大写字母T的形状命名)。这将锁定流,即使其不再直接可用;但是,它将创建两个新的流,称为分支,可以独立使用。发球也很重要,因为无法倒退或重新启动流,稍后将对此进行更多介绍。

可读流是JavaScript的数据源,它由从基础源流过来的ReadableStream对象表示。 ReadableStream()构造函数从给定的处理程序创建并返回一个可读的流对象。有两种类型的基础源:

当您访问数据源时,推送源会不断向您推送数据,这取决于您是否开始,暂停或取消对流的访问。示例包括实时视频流,服务器发送的事件或WebSocket。

拉源要求您在连接到时明确地从它们中请求数据。示例包括通过fetch()或XMLHTTPRequest调用HTTP操作。

流数据在名为块的小块中顺序读取。据说,放置在溪流中的块被排队。这意味着它们在准备读取的队列中等待。内部队列会跟踪尚未读取的块。

排队策略是一个对象,该对象是基于其内部队列的状态来确定流的信号。排队策略为每个块分配大小,并将队列中所有块的总大小与指定的数字进行比较,称为高水标记。

读取器读取流内部的块。此读者一次检索数据一块块,允许您执行任何您想要的操作。读者加上它与之相同的其他处理代码被称为消费者。

此上下文中的下一个构造称为控制器。每个读者都有一个关联的控制器,因为名称建议,允许您控制流。

只有一个读者可以一次读取溪流;创建读取器并开始读取流(即,成为活动读取器),它被锁定到它。如果您希望另一个读者接管阅读您的流,您通常需要在您执行其他任何操作之前释放第一读者(虽然您可以TEE流)。

通过调用其构造函数ReadableStream()创建可读流。构造函数具有一个可选的参数源,它表示具有方法和属性的对象,该对象是定义构造的流实例的行为方式。

start(controller):在构造对象时立即调用。该方法可以访问流源,并执行设置流功能所需的其他任何操作。如果此过程将异步完成,则该方法可以返回一个承诺来表示成功或失败。传递给此方法的控制器参数是ReadableStreamDefaultController。

pull(controller):可以在获取更多块时控制流。只要流的内部块队列未满,就会重复调用它,直到队列达到其高水位线为止。如果调用pull()的结果是一个Promise,则在实现该Promise之前,不会再次调用pull()。如果承诺被拒绝,流将变得错误。

const visibleStream = new({开始(控制器){/ * ... * /},拉(控制器){/ *…* /},取消(原因){/ *…* /},});

ReadableStream()构造函数的第二个参数(也是可选参数)是queuingStrategy。它是一个对象,可以选择定义流的排队策略,该策略采用两个参数:

highWaterMark:一个非负数,表示使用此排队策略的流的高水印。

size(chunk):计算并返回给定块值的有限非负大小的函数。结果通过适当的ReadableStreamDefaultController.desiredSize属性用于确定背压。它还控制何时调用基础source的pull()方法。

const visibleStream = new({/ *…* /},{highWaterMark:10,size(chunk){return chunk.length;},},);

您可以定义自己的自定义queuingStrategy,或为此对象的值使用ByteLengthQueuingStrategy或CountQueuingStrategy的实例。如果未提供queuingStrategy,则使用的默认值与highWaterMark为1的CountQueuingStrategy相同。

要从可读流中进行读取,您需要一个读取器,该读取器将为ReadableStreamDefaultReader。 ReadableStream接口的getReader()方法创建一个读取器并将流锁定到该读取器。当流被锁定时,在释放该阅读器之前,无法获取其他阅读器。

ReadableStreamDefaultReader接口的read()方法返回一个promise,该promise提供对流内部队列中下一个块的访问。它根据流的状态实现或拒绝结果。不同的可能性如下:

如果有可用的块,则将使用{值:块,完成:否}形式的对象来实现承诺。

如果流关闭,则诺言将以{值:未定义,完成:真}形式的对象实现。

const reader =可读流。 getReader(); while(true){const {完成,价值} =等待读者。读 ( ) ;安慰 。 log('只需读取一个块:',value);如果(完成){控制台。 log('流已完成。');休息 ; }}

下面的代码示例显示了所有执行的步骤。您首先创建一个ReadableStream,在其underlyingSource参数中定义了一个start方法,该方法告诉流的控制器在十秒钟内每秒将时间戳加入queue(),然后关闭该流。您可以通过getReader()方法创建一个读取器并调用read()直到流完成来消耗此流。

Warning: Can only detect less than 5000 characters

您可以通过将附加的类型参数传递给ReadableStream()构造函数来创建可读的字节流。

可读字节流的基础源被赋予ReadableByteStreamController进行操作。其ReadableByteStreamController.enqueue()方法采用一个块参数,其值是ArrayBufferView。属性ReadableByteStreamController.byobRequest返回当前的BYOB拉取请求,如果没有,则返回null。最后,ReadableByteStreamController.desiredSize属性返回所需的大小以填充受控流的内部队列。

ReadableStream()构造函数的第二个参数(也是可选参数)是queuingStrategy。它是一个对象,可以选择定义流的排队策略,该策略采用两个参数:

highWaterMark:一个非负数,表示使用此排队策略的流的高水印。

size(chunk):计算并返回给定块值的有限非负大小的函数。结果用于确定反压,通过适当的ReadableByteStreamController.desiredSize属性显示。它还控制何时调用基础source的pull()方法。

您可以定义自己的自定义queuingStrategy,或对此对象值使用ByteLengthQueuingStrategy或CountQueuingStrategy的实例。如果未提供queuingStrategy,则使用的默认值与highWaterMark为1的CountQueuingStrategy相同。

然后,您可以通过相应地设置mode参数来访问ReadableStreamBYOBReader:ReadableStream.getReader({mode:" byob"})。这样可以更精确地控制缓冲区分配,以避免复制。要从字节流中读取数据,您需要调用ReadableStreamBYOBReader.read(view),其中view是ArrayBufferView。

const reader =可读流。 getReader({模式:" byob"});让startingAB = new(1_024); const buffer =等待readInto(startingAB);安慰 。 log("前1024个字节,或更小:",buffer);异步函数readInto(buffer){let offset = 0; while(offset< buffer .byteLength){const {value:view,done} =等待阅读器。读取(新(缓冲区,偏移量,缓冲区.byteLength-偏移量));缓冲=视图.buffer;如果(完成){中断; } offset + = view .byteLength;返回缓冲区; }

以下函数返回可读的字节流,这些字节流允许对随机生成的数组进行有效的零复制读取。它没有使用预定的1,024块大小,而是尝试填充开发人员提供的缓冲区,从而实现完全控制。

const DEFAULT_CHUNK_SIZE = 1_024;函数makeReadableByteStream(){return new({type:' bytes',pull(controller){//即使使用者使用默认读取器,//自动分配功能也会分配一个缓冲区,并且//通过`byobRequest`将其传递给我们。 }

可写流是可在其中写入数据的目标,在JavaScript中由WritableStream对象表示。这是对基础接收器顶部(将原始数据写入其中的较低级别I / O接收器)的抽象。

数据通过写入器一次写入一个块,然后写入到流中。块可以采用多种形式,就像阅读器中的块一样。您可以使用任何喜欢的代码来生成可以编写的块;编写者加上相关代码称为生产者。

创建写程序并开始写到流(活动写程序)时,据说它已锁定到该流。一次只能有一位作家可以写入可写流。如果要其他作者开始向流中写入,通常需要先释放它,然后再将其他作者附加到该流上。

内部队列跟踪已写入流但尚未由基础接收器处理的块。

排队策略是一个对象,该对象根据其内部队列的状态确定流应如何发信号通知背压。 排队策略为每个块分配一个大小,并将队列中所有块的总大小与指定的数字(即高水位标记)进行比较。 最终的构造称为控制器。 每个编写器都有一个关联的控制器,该控制器使您可以控制流(例如,中止它)。 Streams API的WritableStream接口提供了一个标准 ......