Stream API
Stream API 概述
Stream: 该 API 允许 JS 可以以编程的方式访问从服务端接收的数据流,且开发人员可以按需对这些数据流进行一些处理...
-
流会将从网络接收的资源分成许多的小分块(chunk),然后按位进行处理它
→ 这正是浏览器在接收一些用于在 web 显示的资源时所做的事情(如: 视频缓冲和更多的内容可以逐渐进行播放、图片随着内容的加载可以看到图像的逐渐显示等...)
-
在以前的 JS 中是没有办法像上面这样进行处理的
→ 在以前如果想要处理某种资源(如视频、图片、文件文件...),我们必须要先将完整的文件下载下来并等待它反序列化成适当的格式,最后才能完整的接收所有的内容进行处理
-
随着流(stream)在 JS 中的使用,客户端就可以不需要先将整个资源下载下来在进行解析等操作了
→ stream 会将资源分成许多的小分块进行数据的接收,只要在客户端中所接收的分块的原始数据可用,那么就可以通过 JS 按位进行处理,而不在需要缓冲区、字符串或Blob
-
-
我们也可以通过对流进行检测是何时开始或结束,将流链接在一起,根据需要处理错误和取消流,并对流的读取速度做出反应
→ 我们可以通过 Stream 中的接口来对流进行一些操作 → 处理接口 ↓
-
可读流 + ReadableStream + ReadableStreamDefaultReader + ReadableStreamDefaultController 可写流 + WritableStream + WritableStreamDefaultReader + WritableStreamDefaultController 转换流 + TransformStream + TransformStreamDefaultController 流相关的 API 和操作 + ByteLengthQueuingStrategy + CountQueuingStrategy 字节流相关接口 + ReadableStreamBODYReader + ReadableByteStreamController + ReadableStreamBYOBRequest
-
-
其它 API 扩展
Request : 当构造一个新的 Request 对象后,你可以给它的 RequestInit 中的 body 属性传入一个 ReadableStream。这个 Request 对象就可以被传入 fetch() 中,开始接收流
Response.body : 一个成功的 fetch request 响应体会默认暴露为 ReadableStream,从而可以采用相应的 reader 来处理等
Stream 相关概念:
-
可读流:
可读流在 JS 中使用 "ReadableStream" 对象来表示,是一个数据源(数据来源地),对应的数据是从它的底层源流出
→ 底层源: 表示你想要从中获取数据,且是来自网络或其它的域发某种资源(如请求服务器的视频、图片、文本文件等资源)
-
有两种类型的底层源:
-
+ Push source: 会在你访问了它们之后,不断地主动推送数据 → 可以自行开始(start)、暂停(pause)或取消(cancel)对流的访问(例如视频流和 TCP/Web socket) + Pull source: 需要在你连接到它们后,显式地请求数据(例如通过 Fetch 或 XHR 请求访问一个文件)
-
-
数据会按序读入到许多的小分块中
-
-
1. 已放入到流中的分块称已入队(enqueue),上图中 ReadableStream 处的每一个 data 就为已入队的每一个小分块,每一个分块在队列中排队等待被读取 2. 流中的每一个分块都由 render 来进行读取(每次只处理一个分块),在 render 时也允许开发者对分块进行执行任何类型的操作 3. render 加上与他一起运行的其它代码处理,就称之为 consumer(另一个我们将要用到的对象叫做 controller,每一个 render 都有关联一个 controller,来对流进行控制,如关闭流、将分块压入队列等)→ 后续都会用到 4. 一个流一次只能被一个 render 进行读取,当一个 render 被创建并开始读取时,我们称他是 locked(被锁定) 的 + 如果想让另一个 render 读取该流,通常需要先取消该 render,在执行其它操作(或拷贝该流) + 一个内置队列跟踪已经被写入流的分块但是仍然没有被底层接收器处理的分块
-
-
MDN 文档: 有两种不同类型的可读流,除了传统的可读流之外,还有一种类型叫做字节流
这是传统流的扩展版本,用于读取底层字节源
相比于传统的可读流,字节流被允许通过 BYOB reader 读取(BYOB,“带上你自己的缓冲区”)
这种 reader 可以直接将流读入开发者提供的缓冲区,从而最大限度地减少所需的复制
你的代码将使用哪种底层流(以及使用哪种 reader 和 controller)取决于流最初是如何创建的
-
-
拷贝(teeing):
在流的处理中,同一个时刻只能由一个 render 可以读取流,我们可以把流分割成两个相同的副本,这样它们就可以用两个独立的 reader 读取,该过程就称之为 "拷贝"
-
在 JS 中通过 'ReadableStream.tee()' 来实现拷贝操作,该方法返回一个数组,数组中包含原始可读流两个相同的的副本可读流,然后我们就可以通过不同的 render 对其进行处理
-
场景: 在 'ServiceWorker' 中可能需要用到该方法
1. 当从服务器 fetch 一个资源,得到一个响应的可读流
2. 如果想把这个流拆分成两份,一份流入浏览器,一份流入到 ServiceWorker 的缓存 → 但由于响应主体无法被消费两次,以及可读流无法被两个 render 同时读取时
3. 此时,我们可以通过两个可读流副本来实现
-
-
可写流:
可写流用于将数据写入到目的地(某个文件下等),在 JS 中可写流以一个 "WritableStream " 对象表示
-
这是 JS 层面对底层接收器的抽象(操作底层数据) → 一个底层的 I/O 接收器,将原始数据写入其中
-
数据由一个 write 写入流中,每次只能写入块(与可读流的每次只能读取一块同理) → 对应的分块也与可读流的 render 一样可以有多种类型,也可以对 write 中加上一些其它的处理等
-
-
+ 如上图,对 data 数据写入流中的的示例 1. 当 write 被创建并进行对应的写入数据时,当前正在写入的 write 我们称之为是锁定(locked)在该流之上的 2. 这与可读流是类似的,同理,在用一个时刻里面一个 write 只能向一个可写流写入数据 3. 如果想用其它的 wirte 向流中写入数据,那么需要先中止上一个 wirte(这与可读类似,如果想让另一个 render 读取流时,也需要取消先前的 render)
-
-
链式管道传输:
Streams API 中可以通过链式管道(pipe chain)的方式将流传输到另一个流中
→ JS 中主要有如下两种方式可以作用于它
1. ReadableStream.pipeThrough(): 通过转换流(transform stream)传输流,可以在传输的过程中对流进行转换
→ 如可以用于将编码或解码视频帧、压缩或解压缩数据或以其他的方式从一种数据转换成另一种数据
一个转换流(transform stream)由一对流组成: 分别就上面的用于读取数据的可读流与用于写入数据的可写流组成
可以确保新数据一旦写入后即可读取
TransformStream API 就是转换流的一个具体的实现
→ 后续
↑ 虽然说 TransformStream 是转换流的一种具体的实现,但并非只有该 API 对象才可以传递给 "pipeStream()",只要具有相同的可读流和可写流属性的对象都可以传递给 "pipeStream"(可以理解为是 "鸭子类型检测")
2. ReadableStream.pipeTo(): 将数据传输到可写流(注意是可写流,并非是转换流了),且作为链式管道传输的终点
-
背压(backPressure):
是流中一个重要的概念,是单个流或一个链式管道,调节读/写速度的过程
-
背压是指当数据流的生成者生成数据(数据源或数据生成器)超过了消费者(如数据处理单元或数据存储)处理数据的速度,消费者无法及时处理所有传入的数据,导致数据在系统中堆积,进而对生产者形成的一种反向压力 → 这种反向压力就是背压
-
背压的作用:
-
+ 确保系统稳定性:背压机制可以防止系统因数据堆积而崩溃,通过减缓生产者的生产速度或增加消费者的处理能力来保持系统的稳定运行 + 提高资源利用率:通过动态调整生产者和消费者的速度,背压机制可以确保系统资源(如CPU、内存和网络带宽)得到合理利用,避免资源浪费 + 优化系统性能:通过维持生产者和消费者之间的动态平衡,背压机制可以优化系统的整体性能,确保数据流的高效处理
-
-
背压的实现机制 → 在不同的流处理系统中,背压的实现机制可能有所不同,一下是一些常见的实现方式
-
+ 基于缓冲区的背压:在生产者和消费者之间设置一个缓冲区,用于暂时存储无法立即处理的数据。当缓冲区满时,生产者将暂停或减慢数据生成速度,从而实现对消费者的背压 + 动态调整并发度:在一些高级流处理框架(如Apache Flink)中,系统可以根据数据流的压力情况动态调整任务的并发度。当消费者无法及时处理数据时,系统会降低任务的并发度,以减少数据的产生速率 + 基于水位线的流控制:一些流处理系统使用水位线(Watermark)来衡量数据流的进度,并据此调整生产者的生产速度。当水位线指示的数据进度落后于实际处理进度时,系统可以减慢生产者的速度或增加消费者的处理能力 ↓ ▲
-
-
-
内置队列和队列策略:
上面有提到在流中一直没有处理和完成的分块由一个内置的队列跟踪
如 → 可读流一些分块一直在排队,但扔没有被读取 | 写入流一些分块一直被写入,但没有通过底层接收器处理
-
内置的队列采用一个 "队列策略",该策略规定如何基于 "内置队列的状态" 发出背压信号
-
一般来说,该策略会将队列中的分块大小与称作 "high water mark" 的值进行比较 → "high water mark" 是队列期望管理的最大分块的总和
-
执行的计算是: high water mark - total size of chunks in queue = desired size
→
-
1. High Water Mark (HWM): 这是一个阈值或限制,定义了队列可以达到的最大允许大小 + 如当队列中的元素(或称之为 "块" chunk)的总和大小达到或超过这个阈值时,系统可能会采取某些措施来防止队列继续增长 + 比如限制新元素的加入、触发数据刷新到外部存储(如磁盘)、增加数据处理的速率等 2. Total Size of Chunks in Queue: 这指的是当前队列中所有元素(或块)的总大小(该值会随着新元素的加入和旧元素的移除而动态变化) + 是评估队列当前负载和状态的重要指标 3. Desired Size: 不是一个直接由系统设定的具体值,而是一个相对的概念(相对于上面两个),表示系统或算法期望队列保持的一个大小或状态范围
-
上面计算公式的情况分析: + 当 high water mark 大于 total size of chunks in queue 时 - 说明队列还有空间接收更多的元素,系统可能处于正常的工作状态 + 当 total size of chunks in queue 接近或达到 high water mark 时 - 说明当前队列中元素的大小总和,即将到达或超过允许达到的大小(即生成者过快,消费者未来得及进行处理) - 系统可能认为队列即将满载,需要采取措施来防止溢出或优化性能(如加速处理、限制新数据流入等) + Desired Size: - 则可能是一个更灵活的概念,它不一定与 HWM 直接相关,而是基于更广泛的性能和资源管理目标 - 在某些情况下,Desired Size 可能位于 HWM 之下,以确保系统有足够的缓冲空间应对突发流量或负载变化 - 在其他情况下,它可能随着系统状态的变化而动态调整
-
🔺概述 + HWM: 定义了队列的大小上限 + Total Size of Chunks in Queue: 则是当前队列的实际大小 + Desired Size: 则是一个更灵活、更广泛的概念,用于指导系统的整体性能和资源管理策略
-
-