深入探究node之Transform
本文詳細的介紹了node Transform ,分享給大家,希望此文章對各位有所幫助。
Transform流特性
在開發(fā)中直接接觸Transform流的情況不是很多,往往是使用相對成熟的模塊或者封裝的API來完成流的處理,最為特殊的莫過于through2模塊和gulp流操作。那么,Transform流到底有什么特點呢?
從名稱上說,Transform意為處理,類似于生產(chǎn)流水線上的每一道工序,每道工序針對到來的產(chǎn)品作相應(yīng)的處理;從結(jié)構(gòu)上看,Transform是一個雙工流,通俗的解釋它既可以作為可讀流,也可作為可寫流。但是,node卻對Transform流針對其特性做了更為特殊的定制,使Transform不是單純的Duplex流。
Transform流由于包含了Readable和Writeable特性,因此Transform在實際使用中有著多種方式:它既可以只作為消費者消費數(shù)據(jù),也可同時作為生產(chǎn)者和消費者完成數(shù)據(jù)中間處理。下面將逐漸深入內(nèi)部闡述Transform的運行機理及使用技巧。
Transform內(nèi)部架構(gòu)
上圖表示一個Transform實例的組成部分:Readable部分緩沖(數(shù)組)、內(nèi)部_read函數(shù)、Writeable部分緩沖(鏈表)、內(nèi)部_write函數(shù)、Transform實例必須實現(xiàn)的內(nèi)部_transform函數(shù)以及系統(tǒng)提供的回調(diào)函數(shù)afterTransform。由于Transform實例同時擁有兩部分緩沖,因此2個緩沖的存儲、消耗的順序也就需要了解,這對于后面使用原生Transform編寫代碼有很大的指導(dǎo)意義。
傳統(tǒng)意義的流(即Readable和Writeable)的實現(xiàn)者都需要實現(xiàn)對應(yīng)的內(nèi)部函數(shù)_read()和_write(),對于Readable實例而言,_read函數(shù)用于準備從源文件中獲取數(shù)據(jù)并添加到讀緩沖中;對于Writeable實例_write函數(shù)則從寫緩沖鏈表中一次刷入到磁盤中。它們分別對應(yīng)了讀寫流程的首尾步驟,具體可以關(guān)注node中的Stream一文。
而Transform中的_read和_write函數(shù)的實現(xiàn)大有不同,由于需要兼顧流的處理,因此著重分析Transform的內(nèi)部函數(shù)執(zhí)行流程。
示例demo:
readable.pipe(transform);
以上段示例代碼為例,transform作為消費者消費readable。
Transform的實例transform擁有transormState和readableState屬性,保存了相關(guān)屬性,如tranform狀態(tài)信息、回調(diào)函數(shù)存儲和編碼等。transform作為消費者,會在其write函數(shù)中消費數(shù)據(jù),在node中的Stream文中介紹了write函數(shù)的實現(xiàn)細節(jié),通過內(nèi)部調(diào)用_write函數(shù)實現(xiàn)數(shù)據(jù)的寫入。而在Transform中_write函數(shù)已經(jīng)重寫:
1.保存transform收到的chunk數(shù)據(jù)、編碼和函數(shù)(執(zhí)行刷新寫緩沖)
2.在一定條件下執(zhí)行_read函數(shù)(當狀態(tài)為非轉(zhuǎn)換下,只要讀緩沖大小未超過設(shè)定的大小,則執(zhí)行_read)
如果一切順利,readable的數(shù)據(jù)會順利執(zhí)行transform的**write->_write->_read**,那么原本負責填充讀緩沖的_read在Transform中發(fā)生了哪些改變呢?
Transform.prototype._read = function(n) { var ts = this._transformState; if (ts.writechunk !== null && ts.writecb && !ts.transforming) { ts.transforming = true; this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); } else { // mark that we need a transform, so that any data that comes in // will get processed, now that we've asked for it. ts.needTransform = true; } };
可見,_read的實現(xiàn)非常簡單,根據(jù)條件選擇執(zhí)行_transform函數(shù)。需要注意的是_read的參數(shù)n并未有使用,因為是否插入數(shù)據(jù)至讀緩沖是由開發(fā)者在_transform中來決定。相信大家對_transform函數(shù)并不陌生,node規(guī)定Transform實例必須提供_transform函數(shù),而該函數(shù)正是在_read中調(diào)用。
_transform有三個參數(shù),第一個為待處理的chunk數(shù)據(jù),第二個為編碼,第三個為回調(diào)函數(shù)。前兩個參數(shù)很好理解,我們可以在_transform中盡情的處理數(shù)據(jù),最后調(diào)用回調(diào)函數(shù)完成處理。那么,這個回調(diào)函數(shù)究竟是什么? 它就是Transform架構(gòu)圖中的afterTransform函數(shù),它有幾個功能:
1.清空各種狀態(tài)信息,如transformState對象的一些屬性,用于下次處理數(shù)據(jù)使用
2.可選的保存處理結(jié)果至讀緩沖區(qū)
3.刷新寫緩沖區(qū),執(zhí)行下一階段的數(shù)據(jù)流處理
可見,在afterTransform函數(shù)執(zhí)行后,才基本宣告transform第一階段的結(jié)束。為何是第一階段呢?因為transform才完成了作為消費者(即Writeable)的作用,如果用戶在_transform中傳入了數(shù)據(jù)到寫緩沖區(qū),那么此時transform也同時是一個生產(chǎn)者,提供數(shù)據(jù)讓后面的消費者消費數(shù)據(jù),這就涉及到了Transform使用上的問題。
Transform的生產(chǎn)消費實例
const stream = require('stream') var c = 0; const readable = stream.Readable({ highWaterMark: 2, read: function () { var data = c < 26 ? String.fromCharCode(c++ + 97) : null; console.log('push', data); this.push(data); } }) const transform = stream.Transform({ highWaterMark: 2, transform: function (buf, enc, next) { console.log('transform', buf.toString()); next(null, buf); } }) readable.pipe(transform);
示例代碼很簡單,創(chuàng)建了一個可讀流,向消費者提供a-z的小寫字母;創(chuàng)建了一個轉(zhuǎn)換流,在_transform函數(shù)中針對數(shù)據(jù)并不做處理僅作打點輸出,并向回調(diào)函數(shù)傳遞數(shù)據(jù)至讀緩沖區(qū)。我們的目的是通過transform輸出26個小寫字母,但是當前程序執(zhí)行的結(jié)果并不讓人滿意:
執(zhí)行結(jié)果:
push a
push b
transform a
push c
transform b
push d
push e
push f
tranform僅僅處理到字母b,readable也僅僅提供了a-f的數(shù)據(jù)便戛然而止,這是為何?
這一切都歸結(jié)于transform對象。認真讀過上文后我們知道,所有的Transform實例同時有兩個緩沖區(qū),其中寫緩沖區(qū)用來接收生產(chǎn)者的數(shù)據(jù)進行轉(zhuǎn)換操作,讀緩沖區(qū)則緩存數(shù)據(jù)給消費者使用。而在當前的實現(xiàn)中,transform._transform函數(shù)輸出了待處理數(shù)據(jù),同時執(zhí)行next(null, buf);。該函數(shù)上文已有分析,即afterTransform函數(shù),第一個參數(shù)為Error實例,第二個則為存入讀緩沖區(qū)的數(shù)據(jù)。在本例中,執(zhí)行完_transform后將處理后的數(shù)據(jù)存入讀緩沖區(qū),等待后面的消費者消費讀緩沖區(qū)的數(shù)據(jù)??墒?,transform后面沒有消費者了,因此transform在處理完字母b存入讀緩沖區(qū)后,讀緩沖區(qū)已經(jīng)滿了(設(shè)定highWaterMark為2,即讀寫緩沖區(qū)的最大值均為2字節(jié))。當字母c、d也執(zhí)行到tranform._write后,由于不滿足執(zhí)行transform._read的條件無法執(zhí)行transform._transform函數(shù),更無法執(zhí)行afterTransform函數(shù),導(dǎo)致無法刷新寫緩沖區(qū)的數(shù)據(jù),造成字母c、d貯存在寫緩沖區(qū)。而字母e、f則由于transform的寫緩沖區(qū)滿(transform.write()返回false),只有存儲在readable的讀緩沖區(qū)中,等待消費。這就造成了死循環(huán),readable和transform所有的緩沖區(qū)都滿了,流也就停止了。
解決這個問題的方法很簡單,有兩種不同方案:
1.transform的讀緩沖區(qū)保持為空
2.增加消費者消費transform的讀緩沖區(qū)
其實本質(zhì)上都是讓transform的讀緩沖區(qū)得到消耗。
第一種方案:
保證transform的讀緩沖區(qū)為空:
const transform = stream.Transform({ highWaterMark: 2, transform: function (buf, enc, next) { console.log('transform', buf.toString()) next(null, null) } })
只需向next函數(shù)傳入null即可,這樣transform消費完數(shù)據(jù)后即宣告數(shù)據(jù)處理結(jié)束,讀緩沖區(qū)始終為空。
第二種方案:
添加消費者:
const transform = stream.Transform({ highWaterMark: 2, transform: function (buf, enc, next) { console.log('transform', buf.toString()) next(null, buf) } }) readable.pipe(transform).pipe(process.stdout);
transform實現(xiàn)不變,只是添加了消費者process.stdout。這樣也同時保證了transform的讀緩沖區(qū)處于可添加狀態(tài),也給了afterTransform函數(shù)刷新寫緩沖區(qū)的機會,開啟新的數(shù)據(jù)處理流程。
through2的實現(xiàn)
through2的重頭戲在于Transform流,使用through2的API可方便的創(chuàng)建一個Transform實例,完成數(shù)據(jù)流的處理。
function through2 (construct) { return function (options, transform, flush) { if (typeof options == 'function') { flush = transform transform = options options = {} } if (typeof transform != 'function') transform = noop if (typeof flush != 'function') flush = null return construct(options, transform, flush) } } module.exports = through2(function (options, transform, flush) { var t2 = new DestroyableTransform(options) t2._transform = transform if (flush) t2._flush = flush return t2 })
可見,through2模塊僅僅是封裝了Transform的構(gòu)造函數(shù),并封裝了更為易用的objectMode模式。之所以建議使用through2創(chuàng)建Transform對象,不僅僅是因為其提供了方便的API,更主要的是為了兼容性。Transform對象是屬于Stream2.0的特性,早先版本的node并沒有實現(xiàn),而通過through2創(chuàng)建的Transform實例在之前版本的node下仍可正常使用,這是由于through2并未引用node默認提供的stream模塊,而是使用社區(qū)中較為流行的“readable-stream”模塊。
總結(jié)
本文旨在深入through2中的使用的Transform流進行探究,并作為上一篇文章node中的stream的回顧和應(yīng)用。通過文末簡單的示例了解Transform在開發(fā)中可能出現(xiàn)的問題,學(xué)會隨意切換Transform的生產(chǎn)者和消費者的身份,更好的指導(dǎo)實際開發(fā)。
以上所述是小編給大家介紹的node之Transform ,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
- 一行命令搞定node.js 版本升級
- windows系統(tǒng)下簡單nodejs安裝及環(huán)境配置
- 跟我學(xué)Nodejs(一)--- Node.js簡介及安裝開發(fā)環(huán)境
- Node.js(安裝,啟動,測試)
- nodejs中exports與module.exports的區(qū)別詳細介紹
- Node.js實戰(zhàn) 建立簡單的Web服務(wù)器
- nodejs npm install全局安裝和本地安裝的區(qū)別
- NodeJS學(xué)習(xí)筆記之FS文件模塊
- NodeJS學(xué)習(xí)筆記之Http模塊
- nodejs開發(fā)環(huán)境配置與使用
相關(guān)文章
Node.js利用js-xlsx處理Excel文件的方法詳解
這篇文章主要給大家介紹了關(guān)于Node.js利用js-xlsx處理Excel文件的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-07-07NodeJS鏈接MySql數(shù)據(jù)庫的操作方法
下面小編就為大家?guī)硪黄狽odeJS鏈接MySql數(shù)據(jù)庫的操現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06Node.js實現(xiàn)一個HTTP服務(wù)器的方法示例
這篇文章主要介紹了Node.js實現(xiàn)一個HTTP服務(wù)器的方法示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05Node.js學(xué)習(xí)之內(nèi)置模塊fs用法示例
這篇文章主要介紹了Node.js學(xué)習(xí)之內(nèi)置模塊fs用法,結(jié)合實例形式詳細分析了node.js內(nèi)置模塊fs的基本功能、用法與相關(guān)操作注意事項,需要的朋友可以參考下2020-01-01Nodejs基于LRU算法實現(xiàn)的緩存處理操作示例
這篇文章主要介紹了Nodejs基于LRU算法實現(xiàn)的緩存處理操作,結(jié)合具體實例形式分析了LRU算法的原理、功能以及nodejs使用LRU算法實現(xiàn)緩存處理操作的相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-03-03node.js支持多用戶web終端實現(xiàn)及安全方案
這篇文章主要介紹了node.js支持多用戶web終端實現(xiàn)方案以及web終端安全性保證的解決方法,一起學(xué)習(xí)參考下。2017-11-11