Streams API 概念
The Streams API 為 Web 平臺增加了一套非常有用的工具,提供了物件,讓 JavaScript 可以以程式設計方式訪問透過網路接收的資料流,並按開發者期望的方式進行處理。與流相關的一些概念和術語可能對您來說是新的 — 本文將解釋您需要知道的一切。
可讀流
可讀流 (Readable stream) 是一個數據源,在 JavaScript 中由一個 ReadableStream 物件表示,它來自一個底層源 (underlying source) — 這是網路上或其他域中的某個資源,您想從中獲取資料。
底層源有兩種型別
塊 (Chunks)
資料以稱為塊 (chunks) 的小塊形式按順序讀取。一個塊可以是一個位元組,也可以是更大的東西,例如一個特定大小的型別化陣列 (typed array)。單個流可以包含不同大小和型別的塊。

放入流中的塊被稱為已入隊 (enqueued) — 這意味著它們在佇列中等待讀取。內部佇列 (internal queue) 會跟蹤尚未讀取的塊(請參閱下面的“內部佇列和排隊策略”部分)。
讀取器、消費者和控制器
流中的塊由讀取器 (reader) 讀取 — 它一次處理一個塊的資料,允許您對其執行任何您想做的操作。讀取器以及與之相關的其他處理程式碼稱為消費者 (consumer)。
還有一個您將使用的構造稱為控制器 (controller) — 每個讀取器都有一個關聯的控制器,允許您控制流(例如,如果您願意,可以關閉它)。
鎖定
一次只有一個讀取器可以讀取一個流;當建立一個讀取器並開始讀取一個流(一個活動讀取器 (active reader))時,我們說它被鎖定 (locked) 到該流。如果您想讓另一個讀取器開始讀取您的流,您通常需要在做其他事情之前取消第一個讀取器(儘管您可以分流 (tee) 流,請參閱下面的“分流”部分)。
可讀流和位元組流
請注意,可讀流有兩種不同的型別。除了常規的可讀流之外,還有一種稱為位元組流 (byte stream) 的型別 — 它是用於讀取底層位元組源的常規流的擴充套件版本。與常規可讀流相比,位元組流允許由 BYOB 讀取器(BYOB,“bring your own buffer”,自帶緩衝區)讀取。這種讀取器允許流直接讀取到開發者提供的緩衝區中,從而最大限度地減少所需的複製。您的程式碼將使用哪個底層流(以及因此的讀取器和控制器)取決於流最初是如何建立的(請參閱 ReadableStream() 建構函式頁面)。
您可以透過諸如 fetch 請求的 Response.body 之類的機制來利用現成的可讀流,或者使用 ReadableStream() 建構函式來建立您自己的流。
分流 (Teeing)
即使一次只有一個讀取器可以讀取一個流,也可以將一個流分割成兩個相同的副本,然後由兩個獨立的讀取器讀取。這稱為分流 (teeing)。
在 JavaScript 中,這透過 ReadableStream.tee() 方法實現 — 它會輸出一個包含原始可讀流的兩個相同副本的陣列,然後可以由兩個獨立的讀取器獨立讀取。
例如,您可以在 ServiceWorker 中這樣做,如果您想將伺服器響應流式傳輸到瀏覽器,同時也將它流式傳輸到 ServiceWorker 快取。由於響應體不能被消耗多次,而且一個流不能被多個讀取器同時讀取,所以您需要兩個副本才能做到這一點。

可寫流
可寫流 (Writable stream) 是您可以向其中寫入資料的目標,在 JavaScript 中由一個 WritableStream 物件表示。這充當了底層接收器 (underlying sink) 之上的抽象 — 一個用於寫入原始資料的低階 I/O 接收器。
資料透過寫入器 (writer) 一次一個塊地寫入流。一個塊可以採取多種形式,就像讀取器中的塊一樣。您可以使用任何您喜歡的程式碼來生成準備寫入的塊;寫入器加上相關的程式碼稱為生產者 (producer)。
當建立一個寫入器並開始向流寫入(一個活動寫入器 (active writer))時,我們說它被鎖定 (locked) 到該流。一次只有一個寫入器可以寫入可寫流。如果您想讓另一個寫入器開始寫入您的流,您通常需要在附加另一個寫入器之前中止它。
內部佇列 (internal queue) 會跟蹤已寫入流但尚未被底層接收器處理的塊。
還有一個您將使用的構造稱為控制器 — 每個寫入器都有一個關聯的控制器,允許您控制流(例如,如果您願意,可以中止它)。

您可以使用 WritableStream() 建構函式來利用可寫流。目前它們在瀏覽器中的可用性非常有限。
管道鏈
Streams API 允許使用稱為管道鏈 (pipe chain) 的結構將流管道連線在一起。有兩種方法可以實現這一點
-
ReadableStream.pipeThrough()— 將流透過轉換流 (transform stream) 進行管道傳輸,沿途可能轉換資料格式。例如,這可能用於編碼或解碼影片幀、壓縮或解壓縮資料,或以其他方式將資料從一種形式轉換為另一種形式。轉換流由一對流組成:一個用於讀取資料的可讀流和一個用於寫入資料的可寫流,以及適當的機制來確保在資料寫入後立即提供新資料以供讀取。
TransformStream是轉換流的具體實現,但任何具有相同可讀流和可寫流屬性的物件都可以傳遞給pipeThrough()。 -
ReadableStream.pipeTo()— 將資料管道傳輸到一個作為管道鏈終點的可寫流。
管道鏈的開頭稱為原始源 (original source),結尾稱為最終接收器 (ultimate sink)。

背壓
流中的一個重要概念是背壓 (backpressure) — 這是單個流或管道鏈調節讀取/寫入速度的過程。當鏈中後面的流仍然繁忙且尚未準備好接受更多塊時,它會向後傳送訊號,指示前面的轉換流(或原始源)減慢傳輸速度,這樣就不會在任何地方產生瓶頸。
要在 ReadableStream 中使用背壓,我們可以透過查詢控制器上的 ReadableStreamDefaultController.desiredSize 屬性,來詢問控制器想要的塊大小。如果太低,我們的 ReadableStream 可以告訴它的底層源停止傳送資料,然後我們沿著流鏈施加背壓。
如果稍後消費者再次想要接收資料,我們可以使用流建立中的 pull 方法來告訴我們的底層源向我們的流提供資料。
內部佇列和排隊策略
如前所述,流中尚未處理完的塊由內部佇列跟蹤。
- 對於可讀流,這些是已入隊但尚未讀取的塊。
- 對於可寫流,這些是已寫入但尚未被底層接收器處理的塊。
內部佇列採用排隊策略 (queuing strategy),它決定如何根據內部佇列狀態來發出背壓訊號。
通常,該策略將佇列中塊的大小與一個稱為高水位線 (high water mark) 的值進行比較,這是佇列傾向於管理的最大的總塊大小。
執行的計算是
高水位線 - 佇列中塊的總大小 = 期望大小
期望大小 (desired size) 是流仍然可以接受的塊數,以保持流的流動但大小低於高水位線。塊的生成將根據需要減慢/加快,以使流盡可能快地流動,同時保持期望大小大於零。如果該值降至零(或更低),則意味著塊的生成速度快於流的處理能力,這可能會導致問題。
例如,假設塊大小為 1,高水位線為 3。這意味著最多可以入隊 3 個塊,然後達到高水位線並施加背壓。