天天看點

深入node之Transform

在開發中直接接觸Transform流的情況不是很多,往往是使用相對成熟的子產品或者封裝的API來完成流的處理,最為特殊的莫過于through2子產品和gulp流操作。那麼,Transform流到底有什麼特點呢?

從名稱上說,Transform意為處理,類似于生産流水線上的每一道工序,每道工序針對到來的産品作相應的處理;從結構上看,Transform是一個雙工流,通俗的解釋它既可以作為可讀流,也可作為可寫流。但是,node卻對Transform流針對其特性做了更為特殊的定制,使Transform不是單純的Duplex流。

Transform流由于包含了Readable和Writeable特性,是以Transform在實際使用中有着多種方式:它既可以隻作為消費者消費資料,也可同時作為生産者和消費者完成資料中間處理。下面将逐漸深入内部闡述Transform的運作機理及使用技巧。

深入node之Transform

上圖表示一個Transform執行個體的組成部分:Readable部分緩沖(數組)、内部_read函數、Writeable部分緩沖(連結清單)、内部_write函數、Transform執行個體必須實作的内部_transform函數以及系統提供的回調函數afterTransform。由于Transform執行個體同時擁有兩部分緩沖,是以2個緩沖的存儲、消耗的順序也就需要了解,這對于後面使用原生Transform編寫代碼有很大的指導意義。

傳統意義的流(即Readable和Writeable)的實作者都需要實作對應的内部函數_read()和_write(),對于Readable執行個體而言,_read函數用于準備從源檔案中擷取資料并添加到讀緩沖中;對于Writeable執行個體_write函數則從寫緩沖連結清單中一次刷入到磁盤中。它們分别對應了讀寫流程的首尾步驟,具體可以關注node中的Stream一文。

而Transform中的_read和_write函數的實作大有不同,由于需要兼顧流的處理,是以着重分析Transform的内部函數執行流程。

深入node之Transform

以上段示例代碼為例,transform作為消費者消費readable。

Transform的執行個體transform擁有transormState和readableState屬性,儲存了相關屬性,如tranform狀态資訊、回調函數存儲和編碼等。transform作為消費者,會在其write函數中消費資料,在node中的Stream文中介紹了write函數的實作細節,通過内部調用_write函數實作資料的寫入。而在Transform中_write函數已經重寫:

儲存transform收到的chunk資料、編碼和函數(執行重新整理寫緩沖)

在一定條件下執行_read函數(當狀态為非轉換下,隻要讀緩沖大小未超過設定的大小,則執行_read)

如果一切順利,readable的資料會順利執行transform的write->_write->_read,那麼原本負責填充讀緩沖的_read在Transform中發生了哪些改變呢?

可見,_read的實作非常簡單,根據條件選擇執行_transform函數。需要注意的是_read的參數n并未有使用,因為是否插入資料至讀緩沖是由開發者在_transform中來決定。相信大家對_transform函數并不陌生,node規定Transform執行個體必須提供_transform函數,而該函數正是在_read中調用。

_transform有三個參數,第一個為待處理的chunk資料,第二個為編碼,第三個為回調函數。前兩個參數很好了解,我們可以在_transform中盡情的處理資料,最後調用回調函數完成處理。那麼,這個回調函數究竟是什麼? 它就是Transform架構圖中的afterTransform函數,它有幾個功能:

清空各種狀态資訊,如transformState對象的一些屬性,用于下次處理資料使用

可選的儲存處理結果至讀緩沖區

重新整理寫緩沖區,執行下一階段的資料流處理

可見,在afterTransform函數執行後,才基本宣告transform第一階段的結束。為何是第一階段呢?因為transform才完成了作為消費者(即Writeable)的作用,如果使用者在_transform中傳入了資料到讀緩沖區,那麼此時transform也同時是一個生産者,提供資料讓後面的消費者消費資料,這就涉及到了Transform使用上的問題。

示例代碼很簡單,建立了一個可讀流,向消費者提供a-z的小寫字母;建立了一個轉換流,在_transform函數中針對資料并不做處理僅作打點輸出,并向回調函數傳遞資料至讀緩沖區。我們的目的是通過transform輸出26個小寫字母,但是目前程式執行的結果并不讓人滿意:

tranform僅僅處理到字母b,readable也僅僅提供了a-f的資料便戛然而止,這是為何?

這一切都歸結于transform對象。認真讀過上文後我們知道,所有的Transform執行個體同時有兩個緩沖區,其中寫緩沖區用來接收生産者的資料進行轉換操作,讀緩沖區則緩存資料給消費者使用。而在目前的實作中,transform._transform函數輸出了待處理資料,同時執行next(null, buf);。該函數上文已有分析,即afterTransform函數,第一個參數為Error執行個體,第二個則為存入讀緩沖區的資料。在本例中,執行完_transform後将處理後的資料存入讀緩沖區,等待後面的消費者消費讀緩沖區的資料。可是,transform後面沒有消費者了,是以transform在處理完字母b存入讀緩沖區後,讀緩沖區已經滿了(設定highWaterMark為2,即讀寫緩沖區的最大值均為2位元組)。當字母c、d也執行到tranform._write後,由于不滿足執行transform._read的條件無法執行transform._transform函數,更無法執行afterTransform函數,導緻無法重新整理寫緩沖區的資料,造成字母c、d貯存在寫緩沖區。而字母e、f則由于transform的寫緩沖區滿(transform.write()傳回false),隻有存儲在readable的讀緩沖區中,等待消費。這就造成了死循環,readable和transform所有的緩沖區都滿了,流也就停止了。

解決這個問題的方法很簡單,有兩種不同方案:

transform的讀緩沖區保持為空

增加消費者消費transform的讀緩沖區

其實本質上都是讓transform的讀緩沖區得到消耗。

第一種方案:

隻需向next函數傳入null即可,這樣transform消費完資料後即宣告資料處理結束,讀緩沖區始終為空。

第二種方案:

transform實作不變,隻是添加了消費者process.stdout。這樣也同時保證了transform的讀緩沖區處于可添加狀态,也給了afterTransform函數重新整理寫緩沖區的機會,開啟新的資料處理流程。

through2的重頭戲在于Transform流,使用through2的API可友善的建立一個Transform執行個體,完成資料流的處理。

可見,through2子產品僅僅是封裝了Transform的構造函數,并封裝了更為易用的objectMode模式。之是以建議使用through2建立Transform對象,不僅僅是因為其提供了友善的API,更主要的是為了相容性。Transform對象是屬于Stream2.0的特性,早先版本的node并沒有實作,而通過through2建立的Transform執行個體在之前版本的node下仍可正常使用,這是由于through2并未引用node預設提供的stream子產品,而是使用社群中較為流行的“readable-stream”子產品。

本文旨在深入through2中的使用的Transform流進行探究,并作為上一篇文章node中的stream的回顧和應用。通過文末簡單的示例了解Transform在開發中可能出現的問題,學會随意切換Transform的生産者和消費者的身份,更好的指導實際開發。