流(stream),看一個人流不流逼,就看你對流的理解了
學習本無底,前進莫徬徨
今天跟大家分享的是node.js中的流(stream
)。它的作用大家應該都在平常使用node的時候看到過,比如:
gulp
中的pipe就是流的一種方法,通過可寫流和可讀流的配合,達到不佔用多餘快取的一種讀寫方式。- express和koa中的res和req也是流,res是
可寫流
,req是可讀流
,他們都是通過封裝node中的net模組的socket(雙工流
,即可寫、可讀流)而來的。 - 。。。
可能很多時候大家都知道怎麼用,但不瞭解它的原理,很尷尬,就像這樣
何謂流?
- 流是一組有序的,有起點和終點的位元組資料傳輸手段。
- 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理。
- 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
- 流被分為
Readable
(可讀流)、Writable
(可寫流)、Duplex
(雙工流)、Transform
(轉換流)
流中的是什麼?
二進位制模式
:每個分塊都是buffer、string物件。物件模式
:流內部處理的是一系列普通物件。
可讀流
可讀流分為
flowing
和paused
兩種模式
引數
path
:讀取的檔案的路徑option
:highWaterMark
:水位線,一次可讀的位元組,一般預設是64k
flags
:標識,開啟檔案要做的操作,預設是r
encoding
:編碼,預設為bufferstart
:開始讀取的索引位置end
:結束讀取的索引位置(包括結束位置)autoClose
:讀取完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')
//讀取的時候預設讀64k
let rs = new ReadStream('./a.txt',{
highWaterMark: 2,//一次讀的位元組 預設64k
flags: 'r', //標示 r為讀 w為寫
autoClose: true, //預設讀取完畢後自動關閉
start: 0,
end: 5, //流是閉合區間包start,也包end 預設是讀完
encoding: 'utf8' //預設編碼是buffer
})
複製程式碼
方法
data
:切換到流動模式,可以流出資料
rs.on('data', function (data) {
console.log(data);
});
複製程式碼
open
:流開啟檔案的時候會觸發此監聽
rs.on('open', function () {
console.log('檔案被開啟');
});
複製程式碼
error
:流出錯的時候,監聽錯誤資訊
rs.on('error', function (err) {
console.log(err);
});
複製程式碼
end
:流讀取完成,觸發end
rs.on('end', function (err) {
console.log('讀取完成');
});
複製程式碼
close
:關閉流,觸發
rs.on('close', function (err) {
console.log('關閉');
});
複製程式碼
pause
:暫停流(改變流的flowing,不讀取資料了);resume
:恢復流(改變流的flowing,繼續讀取資料)
//流通過一次後,停止流動,過了2s後再動
rs.on('data', function (data) {
rs.pause();
console.log(data);
});
setTimeout(function () {
rs.resume();
},2000);
複製程式碼
fs.read()
:可讀流底層呼叫的就是這個方法,最原生的讀方法
//fd檔案描述符,一般通過fs.open中獲取
//buffer是讀取後的資料放入的快取目標
//0,從buffer的0位置開始放入
//BUFFER_SIZE,每次放BUFFER_SIZE這麼長的長度
//index,每次從檔案的index的位置開始讀
//bytesRead,真實讀到的個數
fs.read(fd,buffer,0,BUFFER_SIZE,index,function(err,bytesRead){
})
複製程式碼
那讓我們自己來實現一個可愛
的讀流吧!
let fs = require('fs')
let EventEmitter = require('events')
class ReadStream extends EventEmitter{
constructor(path,options = {}){
super()
this.path = path
this.highWaterMark = options.highWaterMark || 64*1024
this.flags = options.flags || 'r'
this.start = options.start || 0
this.pos = this.start //會隨著讀取的位置改變
this.autoClose = options.autoClose || true
this.end = options.end || null
//預設null就是buffer
this.encoding = options.encoding || null
//引數的問題
this.flowing = null //非流動模式
//建立個buffer用來儲存每次讀出來的資料
this.buffer = Buffer.alloc(this.highWaterMark)
//開啟這個檔案
this.open()
//此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
this.on('newListener',(type)=>{// 等待著他監聽data事件
if(type === 'data'){
this.flowing = true
//開始讀取 客戶已經監聽的data事件
this.read()
}
})
}
//預設第一次呼叫read方法時fd還沒獲取 所以不能直接讀
read(){
if(typeof this.fd != 'number'){
//等待著觸發open事件後fd肯定拿到了 再去執行read方法
return this.once('open',()=>{this.read()})
}
//每次讀的時候都要判斷一下下次讀幾個 如果沒有end就根據highWaterMark來(讀所有的) 如果有且大於highWaterMark就根據highWaterMark來 如果小於highWaterMark就根據end來
let howMuchToRead = this.end?Math.min(this.end - this.pos + 1,this.highWaterMark):this.highWaterMark
fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,byteRead)=>{
this.pos += byteRead
let b = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead)
this.emit('data',b)
//如果讀取到的數量和highWaterMark一樣 說明還得繼續讀
if((byteRead === this.highWaterMark)&&this.flowing){
this.read()
}
if(byteRead < this.highWaterMark){
this.emit('end')
this.destory()
}
})
}
destory(){
if(typeof this.fd != 'number'){
return this.emit('close')
}
//如果檔案被開啟過 就關閉檔案並且觸發close事件
fs.close(this.fd,()=>{
this.emit('close')
})
}
pause(){
this.flowing = false
}
resume(){
this.flowing = true
this.read()
}
open(){
//fd表示的就是當前this.path的這個檔案,從3開始(number型別)
fs.open(this.path,this.flags,(err,fd)=>{
//有可能fd這個檔案不存在 需要做處理
if(err){
//如果有自動關閉 則幫他銷燬
if(this.autoClose){
//銷燬(關閉檔案,觸發關閉檔案事件)
this.destory()
}
//如果有錯誤 就會觸發error事件
this.emit('error',err)
return
}
//儲存檔案描述符
this.fd = fd
//當檔案開啟成功時觸發open事件
this.emit('open',this.fd)
})
}
}
複製程式碼
Readable
這個方法是可讀流的一種
暫停模式
,他的模式可以參考為讀流是往水杯倒水的人,Readable是喝水的人,他們之間存在著一種聯絡,只要Readable喝掉一點水,讀流就會繼續往裡倒
。
Readable是什麼?
- 他會在剛開始監聽Readable的時候就觸發流的,此時流就會讀取一次資料,之後
流會監聽,如果有人讀過流(喝過水),並且減少,就會再去讀一次(倒點水)
- 主要可以用來做
行讀取器(LineReader)
let fs = require('fs')
let read = require('./ReadableStream')
let rs = fs.createReadStream('./a.txt', {
//每次讀7個
highWaterMark: 7
})
//如果讀流第一次全部讀下來並且小於highWaterMark,就會再讀一次(再觸發一次readable事件)
//如果rs.read()不加引數,一次性讀完,會從快取區再讀一次,為null
//如果readable每次都剛好讀完(即rs.read()的引數剛好和highWaterMark相等),就會一直觸發readable事件,如果最後不足他想喝的數,他就會先觸發一次null,最後把剩下的喝完
//一開始快取區為0的時候也會預設調一次readable事件
rs.on('readable', () => {
let result = rs.read(2)
console.log(result)
})
複製程式碼
實戰:行讀取器(平常我們的檔案可能有回車、換行,此時如果要每次想讀一行的資料,就得用到readable
)
let EventEmitter = require('events')
//如果要將內容全部讀出就用on('data'),精確讀取就用on('readable')
class LineReader extends EventEmitter {
constructor(path) {
super()
this.rs = fs.createReadStream(path)
//回車符的十六進位制
let RETURN = 0x0d
//換行符的十六進位制
let LINE = 0x0a
let arr = []
this.on('newListener', (type) => {
if (type === 'newLine') {
this.rs.on('readable', () => {
let char
//每次讀一個,當讀完的時候會返回null,終止迴圈
while (char = this.rs.read(1)) {
switch (char[0]) {
case RETURN:
break;
//Mac下只有換行符,windows下是回車符和換行符,需要根據不同的轉換。因為我這裡是Mac
case LINE:
//如果是換行符就把陣列轉換為字串
let r = Buffer.from(arr).toString('utf8')
//把陣列清空
arr.length = 0
//觸發newLine事件,把得到的一行資料輸出
this.emit('newLine', r)
break;
default:
//如果不是換行符,就放入陣列中
arr.push(char[0])
}
}
})
}
})
//以上只能取出之前的換行符前的程式碼,最後一行的後面沒有換行符,所以需要特殊處理。當讀流讀完需要觸發end事件時
this.rs.on('end', () => {
//取出最後一行資料,轉成字串
let r = Buffer.from(arr).toString('utf8')
arr.length = 0
this.emit('newLine', r)
})
}
}
let lineReader = new LineReader('./a.txt')
lineReader.on('newLine', function (data) {
console.log(data)
})
複製程式碼
那麼Readable到底是怎樣的存在呢?我們接下來實現他的原始碼,看看內部到底怎麼回事
let fs = require('fs')
let EventEmitter = require('events')
class ReadStream extends EventEmitter{
constructor(path,options = {}){
super()
this.path = path
this.highWaterMark = options.highWaterMark || 64*1024
this.flags = options.flags || 'r'
this.start = options.start || 0
this.pos = this.start //會隨著讀取的位置改變
this.autoClose = options.autoClose || true
this.end = options.end || null
//預設null就是buffer
this.encoding = options.encoding || null
//引數的問題
this.reading = false //非流動模式
//建立個buffer用來儲存每次讀出來的資料
this.buffers = []
//快取區長度
this.len = 0
//是否要觸發readable事件
this.emittedReadable = false
//觸發open獲取檔案的fd識別符號
this.open()
//此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
this.on('newListener',(type)=>{// 等待著他監聽data事件
if(type === 'readable'){
//開始讀取 客戶已經監聽的data事件
this.read()
}
})
}
//readable真正的原始碼中的方法,計算出和n最接近的2的冪次數
computeNewHighWaterMark(n) {
n--;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n++;
return n;
}
read(n){
//當讀的數量大於水平線,會通過取2的冪次取比他大和最接近的數
if(this.len < n){
this.highWaterMark = this.computeNewHighWaterMark(n)
//重新觸發readbale的callback,所以第一次會觸發null
this.emittedReadable = true
//重新讀新的水位線
this._read()
}
//真正讀取到的
let buffer = null
//說明快取裡有這麼多,取出來
if(n>0 && n<=this.len){
//定義一個buffer
buffer = Buffer.alloc(n)
let buf
let flag = true
let index = 0
//[buffer<1,2,3,4>,buffer<1,2,3,4>,buffer<1,2,3,4>]
//每次取出快取前的第一個buffer
while(flag && (buf = this.buffers.shift())){
for(let i=0;i<buf.length;i++){
//把取出的一個buffer中的資料放入新定義的buffer中
buffer[index++] = buf[i]
//當buffer的長度和n(引數)長度一樣時,停止迴圈
if(index === n){
flag = false
//維護快取,因為可能快取中的buffer長度大於n,當取出n的長度時,還會剩下其餘的buffer,我們需要切割buf並且放到快取陣列之前
this.len -= n
let r = buf.slice(i+1)
if(r.length){
this.buffers.unshift(r)
}
break
}
}
}
}
//如果快取區沒有東西,等會讀完需要觸發readable事件
//這裡會有一種狀況,就是如果每次Readable讀取的數量正好等於highWaterMark(流讀取到快取的長度),就會每次都等於0,每次都觸發Readable事件,就會每次讀,讀到沒有為止,最後還會觸發一下null
if(this.len === 0){
this.emittedReadable = true
}
if(this.len < this.highWaterMark){
//預設,一開始的時候開始讀取
if(!this.reading){
this.reading = true
//真正多讀取操作
this._read()
}
}
return buffer&&buffer.toString()
}
_read(){
if(typeof this.fd != 'number'){
//等待著觸發open事件後fd肯定拿到了 再去執行read方法
return this.once('open',()=>{this._read()})
}
//先讀這麼多buffer
let buffer = Buffer.alloc(this.highWaterMark)
fs.read(this.fd,buffer,0,buffer.length,this.pos,(err,byteRead)=>{
if(byteRead > 0){
//當第一次讀到資料後,改變reading的狀態,如果觸發read事件,可能還會在觸發第二次_read
this.reading = false
//每次讀到資料增加快取取得長度
this.len += byteRead
//每次讀取之後,會增加讀取的檔案的讀取開始位置
this.pos += byteRead
//將讀到的buffer放入快取區buffers中
this.buffers.push(buffer.slice(0,byteRead))
//觸發readable
if(this.emittedReadable){
this.emittedReadable = false
//可以讀取了,預設開始的時候杯子填滿了
this.emit('readable')
}
}else{
//沒讀到就出發end事件
this.emit('end')
}
})
}
destory(){
if(typeof this.fd != 'number'){
return this.emit('close')
}
//如果檔案被開啟過 就關閉檔案並且觸發close事件
fs.close(this.fd,()=>{
this.emit('close')
})
}
open(){
//fd表示的就是當前this.path的這個檔案,從3開始(number型別)
fs.open(this.path,this.flags,(err,fd)=>{
//有可能fd這個檔案不存在 需要做處理
if(err){
//如果有自動關閉 則幫他銷燬
if(this.autoClose){
//銷燬(關閉檔案,觸發關閉檔案事件)
this.destory()
}
//如果有錯誤 就會觸發error事件
this.emit('error',err)
return
}
//儲存檔案描述符
this.fd = fd
//當檔案開啟成功時觸發open事件
this.emit('open',this.fd)
})
}
}
複製程式碼
- Readable和讀流的data的區別就是,Readable可以控制自己從快取區讀多少和控制讀的次數,而data是每次讀取都清空快取,讀多少輸出多少
- 我們可以看一下下面這個例子
let rs = fs.createReadStream('./a.txt')
rs.on('data',(data)=>{
console.log(data)
})
//因為上面的data事件把資料讀了,清空快取區。所以導致下面的readable讀出為null
rs.on('readable',()=>{
let result = r.read(1)
console.log(result)
})
複製程式碼
自定義可讀流
因為createReadStream
內部呼叫了ReadStream
類,ReadStream
又實現了Readable
介面,ReadStream
實現了_read()
方法,所以我們通過自定義一個類繼承stream
模組的Readable
,並在原型
上自定義一個_read()
就可以自定義自己的可讀流
let { Readable } = require('stream')
class MyRead extends Readable{
//流需要一個_read方法,方法中push什麼,外面就接收什麼
_read(){
//push方法就是上面_read方法中的push一樣,把資料放入快取區中
this.push('100')
//如果push了null就表示沒有東西可讀了,停止(如果不寫,就會一直push上面的值,死迴圈)
this.push(null)
}
}
複製程式碼
可寫流
- 如果檔案不存在會建立,如果有內容會被清空
- 讀取到highWaterMark的時候就會輸出
- 第一次是真的寫到檔案 後面就是寫入快取區 再從快取區裡面去取
引數(和可讀流的類似)
path
:寫入的檔案的路徑option
:highWaterMark
:水位線,一次可寫入快取中的位元組,一般預設是64k
flags
:標識,寫入檔案要做的操作,預設是w
encoding
:編碼,預設為bufferstart
:開始寫入的索引位置end
:結束寫入的索引位置(包括結束位置)autoClose
:寫入完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')
//讀取的時候預設讀64k
let rs = new ReadStream('./a.txt',{
highWaterMark: 2,//一次讀的位元組 預設64k
flags: 'r', //標示 r為讀 w為寫
autoClose: true, //預設讀取完畢後自動關閉
start: 0,
end: 5, //流是閉合區間包start,也包end 預設是讀完
encoding: 'utf8' //預設編碼是buffer
})
複製程式碼
方法
write
let fs = require('fs')
let ws = fs.createWriteStream('./d.txt',{
flags: 'w',
encoding: 'utf8',
start: 0,
//write的highWaterMark只是用來觸發是不是幹了
highWaterMark: 3 //寫是預設16k
})
//返回boolean 每當write一次都會在ws中吃下一個饅頭 當吃下的饅頭數量達到highWaterMark時 就會返回false 吃不下了會把其餘放入快取 其餘狀態返回true
//write只能放string或者buffer
flag = ws.write('1','utf8',()=>{
console.log(i)
})
複製程式碼
drain
//drain只有嘴塞滿了 吃完(包括記憶體中的,就是地下的)才會觸發 這裡是兩個條件 一個是必須是吃下highWaterMark個饅頭 並且在吃完的時候才會callback
ws.on('drain',()=>{
console.log('幹了')
})
複製程式碼
fs.write()
:可讀流底層呼叫的就是這個方法,最原生的讀方法
//wfd檔案描述符,一般通過fs.open中獲取
//buffer,要取資料的快取源
//0,從buffer的0位置開始取
//BUFFER_SIZE,每次取BUFFER_SIZE這麼長的長度
//index,每次寫入檔案的index的位置
//bytesRead,真實寫入的個數
fs.write(wfd,buffer,0,bytesRead,index,function(err,bytesWrite){
})
複製程式碼
通過程式碼實現
let fs = require('fs')
let EventEmitter = require('events')
//只有第一次write的時候直接用_write寫入檔案 其餘都是放到cache中 但是len超過了highWaterMark就會返回false告知需要drain 很佔快取
//從第一次的_write開始 回去一直通過clearBuffer遞迴_write寫入檔案 如果cache中沒有了要寫入的東西 會根據needDrain來判斷是否觸發乾點
class WriteStream extends EventEmitter{
constructor(path,options = {}){
super()
this.path = path
this.highWaterMark = options.highWaterMark || 64*1024
this.flags = options.flags || 'r'
this.start = options.start || 0
this.pos = this.start
this.autoClose = options.autoClose || true
this.mode = options.mode || 0o666
//預設null就是buffer
this.encoding = options.encoding || null
//開啟這個檔案
this.open()
//寫檔案的時候需要哪些引數
//第一次寫入的時候 是給highWaterMark個饅頭 他會硬著頭皮寫到檔案中 之後才會把多餘吃不下的放到快取中
this.writing = false
//快取陣列
this.cache = []
this.callbackList = []
//陣列長度
this.len = 0
//是否觸發drain事件
this.needDrain = false
}
clearBuffer(){
//取快取中最上面的一個
let buffer = this.cache.shift()
if(buffer){
//有buffer的情況下
this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer(),buffer.callback)
}else{
//沒有的話 先看看需不需要drain
if(this.needDrain){
//觸發drain 並初始化所有狀態
this.writing = false
this.needDrain = false
this.callbackList.shift()()
this.emit('drain')
}
this.callbackList.map(v=>{
v()
})
this.callbackList.length = 0
}
}
_write(chunk,encoding,clearBuffer,callback){
//因為write方法是同步呼叫的 所以可能還沒獲取到fd
if(typeof this.fd != 'number'){
//直接在open的時間物件上註冊一個一次性事件 當open被emit的時候會被呼叫
return this.once('open',()=>this._write(chunk,encoding,clearBuffer,callback))
}
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWrite)=>{
this.pos += byteWrite
//每次寫完 相應減少記憶體中的數量
this.len -= byteWrite
if(callback) this.callbackList.push(callback)
//第一次寫完
clearBuffer()
})
}
//寫入方法
write(chunk,encoding=this.encoding,callback){
//判斷chunk必須是字串或者buffer 為了統一都變成buffer
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding)
//維護快取的長度 3
this.len += chunk.length
let ret = this.len < this.highWaterMark
if(!ret){
//表示要觸發drain事件
this.needDrain = true
}
//正在寫入的應該放到記憶體中
if(this.writing){
this.cache.push({
chunk,
encoding,
callback
})
}else{
//這裡是第一次寫的時候
this.writing = true
//專門實現寫的方法
this._write(chunk,encoding,()=>this.clearBuffer(),callback)
}
// console.log(ret)
//能不能繼續寫了 false代表下次寫的時候更佔記憶體
return ret
}
destory(){
if(typeof this.fd != 'number'){
return this.emit('close')
}
//如果檔案被開啟過 就關閉檔案並且觸發close事件
fs.close(this.fd,()=>{
this.emit('close')
})
}
open(){
//fd表示的就是當前this.path的這個檔案,從3開始(number型別)
fs.open(this.path,this.flags,(err,fd)=>{
//有可能fd這個檔案不存在 需要做處理
if(err){
//如果有自動關閉 則幫他銷燬
if(this.autoClose){
//銷燬(關閉檔案,出發關閉檔案事件)
this.destory()
}
//如果有錯誤 就會觸發error事件
this.emit('error',err)
return
}
//儲存檔案描述符
this.fd = fd
//當檔案開啟成功時觸發open事件
this.emit('open',this.fd)
})
}
}
複製程式碼
自定義可寫流
因為createWriteStream
內部呼叫了WriteStream
類,WriteStream
又實現了Writable
介面,WriteStream
實現了_write()
方法,所以我們通過自定義一個類繼承stream
模組的Writable
,並在原型
上自定義一個_write()
就可以自定義自己的可寫流
let { Writable } = require('stream')
class MyWrite extends Writable{
_write(chunk,encoding,callback){
//write()的第一個引數,寫入的資料
console.log(chunk)
//這個callback,就相當於我們上面的clearBuffer方法,如果不執行callback就不會繼續從快取中取出寫
callback()
}
}
let write = new MyWrite()
write.write('1','utf8',()=>{
console.log('ok')
})
複製程式碼
pipe
管道流,是可讀流上的方法,至於為什麼放到這裡,主要是因為需要2個流的基礎知識,是可讀流配合可寫流的一種
傳輸方式
。如果用原來的讀寫,因為寫比較耗時
,所以會多讀少寫
,耗記憶體
,但用了pipe
就不會了,始終用規定
的記憶體。
用法
let fs = require('fs')
//pipe方法叫管道 可以控制速率
let rs = fs.createReadStream('./d.txt',{
highWaterMark: 4
})
let ws = fs.createWriteStream('./e,txt',{
highWaterMark: 1
})
//會監聽rs的on('data')將讀取到的資料,通過ws.write的方法寫入檔案
//呼叫寫的一個方法 返回boolean型別
//如果返回false就呼叫rs的pause方法 暫停讀取
//等待可寫流 寫入完畢在監聽drain resume rs
rs.pipe(ws) //會控制速率 防止淹沒可用記憶體
複製程式碼
自己實現一下
let fs = require('fs')
//這兩個是上面自己寫的ReadStream和WriteStream
let ReadStream = require('./ReadStream')
let WriteStream = require('./WriteStream')
//如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫,耗記憶體
ReadStream.prototype.pipe = function(dest){
this.on('data',(data)=>{
let flag = dest.write(data)
//如果寫入的時候嘴巴吃滿了就不繼續讀了,暫停
if(!flag){
this.pause()
}
})
//如果寫的時候嘴巴里的吃完了,就會繼續讀
dest.on('drain',()=>{
this.resume()
})
this.on('end',()=>{
this.destory()
//清空快取中的資料
fs.fsync(dest.fd,()=>{
dest.destory()
})
})
}
複製程式碼
雙工流
有了雙工流,我們可以在同一個物件上
同時實現可讀和可寫
,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立
於彼此。這僅僅是將兩個特性組合成一個物件。
let { Duplex } = require('stream')
//雙工流,可讀可寫
class MyDuplex extends Duplex{
_read(){
this.push('hello')
this.push(null)
}
_write(chunk,encoding,clearBuffer){
console.log(chunk)
clearBuffer()
}
}
let myDuplex = new MyDuplex()
//process.stdin是node自帶的process程式中的可讀流,會監聽命令列的輸入
//process.stdout是node自帶的process程式中的可寫流,會監聽並輸出在命令列中
//所以這裡的意思就是在命令列先輸出hello,然後我們輸入什麼他就出來對應的buffer(先作為可讀流出來)
process.stdin.pipe(myDuplex).pipe(process.stdout)
複製程式碼
轉換流
轉換流的輸出是從輸入中
計算
出來的。對於轉換流,我們不必實現read
或write
的方法,我們只需要實現一個transform
方法,將兩者結合起來。它有write
方法的意思,我們也可以用它來push
資料。
let { Transform } = require('stream')
class MyTransform extends Transform{
_transform(chunk,encoding,callback){
console.log(chunk.toString().toUpperCase())
callback()
}
}
let myTransform = new MyTransform()
class MyTransform2 extends Transform{
_transform(chunk,encoding,callback){
console.log(chunk.toString().toUpperCase())
this.push('1')
// this.push(null)
callback()
}
}
let myTransform2 = new MyTransform2()
//此時myTransform2被作為可寫流觸發_transform,輸出輸入的大寫字元後,會通過可讀流push字元到下一個轉換流中
//當寫入的時候才會觸發transform的值,此時才會push,所以後面的pipe拿到的chunk是前面的push的值
process.stdin.pipe(myTransform2).pipe(myTransform)
複製程式碼
總結
可讀流
- 在 flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。
- 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。
- 所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式:
- 監聽 'data' 事件
- 呼叫 stream.resume() 方法
- 呼叫 stream.pipe() 方法將資料傳送到 Writable
- 可讀流可以通過下面途徑切換到 paused 模式:
- 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
- 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。
可寫流
- 需要知道只有在
嘴
真正的吃滿了,並且等到把嘴裡的和地上的饅頭(快取中的)都吃下了才會觸發drain
事件 - 第一次寫入會直接寫入檔案中,後面會從快取中一個個取
雙工流
- 只是對可寫可讀流的一種應用,既可作為可讀流,也能作為可寫流,並且作為可讀或者可寫時時
隔離
的
轉換流
- 一般轉換流是邊輸入邊輸出的,而且一般只有觸發了寫入操作時才會進入
_transform
方法中。跟雙工流的區別就是,他的可讀可寫是在一起
的。
OK,講完收工,從此你就是流
魔王