node
流
可讀流
流是Node.js最重要的組成和設計模式之一,同時也是最容易讓人產生誤解的地方。流,不僅因為其在技術層面表現出的良好效能和高效率,更因為他的優雅,以及能完美契合Node.js的變成思想。
流為什麼很重要
我們都知道,Node.js是以事件為基礎的,實時處理是處理I/O操作最高效的方法,儘快接收輸入內容,並經過程式處理儘快輸出結果。
那什麼是流呢?
流是一組有序的,有起點和終點的位元組資料傳輸手段 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
從Node.js的核心模組開始,流是隨處可見的。比如,fs模組使用的createReadStream()方法讀取檔案,使用createWriteStream()來寫檔案,HTTP的request和response物件本質來說也是流。
流的分類:四種基本的流型別 每個流的例項都是stream模組提供的四個基本抽象類之一的實現:
- Readable - 可讀的流 (例如 fs.createReadStream()).
- Writable - 可寫的流 (例如 fs.createWriteStream()).
- Duplex - 可讀寫的流 (例如 net.Socket).
- Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate()). 實際上,流可以產生好幾類事件,比如在可讀流完成讀取之後會產生end事件,或者在發生錯誤時產生error事件。
流中的資料有兩種模式,二進位制模式和物件模式.
二進位制模式, 每個分塊都是buffer或者string物件. 物件模式, 流內部處理的是一系列普通物件.
所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。
現在先從簡單入手,先聊聊可讀流~
可讀流
可讀流讀取資料的兩種模式:流動模式(flowing
) 和暫停模式(paused
)
在flowing模式下,從流中讀取資料的方式是給data事件新增一個監聽器,在該模式下資料不是通過read()來獲取。而是,只要流中的資料可讀,便會立即被推送到data事件的監聽器。 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。 所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式:
- 監聽 'data' 事件
- 呼叫 stream.resume() 方法
- 呼叫 stream.pipe() 方法將資料傳送到 Writable
可讀流可以通過下面途徑切換到 paused 模式:
- 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
- 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。
如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 'data' 事件,或是取消了 'data' 事件監聽,就有可能出現這種情況。
實現可讀流
現在,先實現一個可讀流的例子。
/*
var rs = fs.createReadStream(path,[options]);
path讀取檔案的路徑
options
flags開啟檔案要做的操作,預設為'r'
encoding預設為null
start開始讀取的索引位置
end結束讀取的索引位置(包括結束位置)
highWaterMark讀取快取區預設的大小64kb
*/
let fs = require('fs');
let path = require('path');
let rs = fs.createReadStream(path.join(__dirname,'檔名'),{// 返回的是一個可讀流物件
flags:'r',// 檔案的操作是讀取操作
encoding:'utf8',// 預設是null null代表的是buffer
autoClose:true,// 讀取完畢後自動關閉
highWaterMark:3,//最高水位線 預設是64k 64*1024b
start:0,//檔案是有序的,從哪讀到哪,可以自己定,從0開始讀
end:9,//讀10個數,包前又包後,0-9,既有前面又有後面
});
//rs.setEncoding('utf8');//設定讀取的編碼格式,一般是utf8,與指定{encoding:'utf8'}效果相同,設定編碼
// 預設情況下 不會將檔案中的內容輸出
// 也就是說把這個流往這裡一放,它不會自動讀檔案
// 內部會先建立一個buffer先讀取3b,因為先把highWaterMark的值為3,這個檔案不會全讀了先放在這,什麼都不動。這種模式叫非流動模式 / 暫停模式
// 那什麼時候開始真正工作呢?你需要幹一件事。這個流是基於事件的,所以我們要先監聽一下事件,事件名是內建好的
// 操作的是一個檔案,需要把檔案開啟
rs.on('open',function(){//監聽open事件
console.log('檔案開啟');
})
// 檔案有開啟就有關閉
rs.on('close',function(){
console.log('檔案關閉');
});
// 因為事件都是非同步的,觸發的時候會觸發回撥函式,可能會有一些錯誤,錯誤需要捕獲
rs.on('error',function(err){//監聽error事件
console.log(err);
});
rs.on('data',function(data){// 監聽了data事件,流就從暫停模式->流動模式,但流動模式會瘋狂觸發data事件,不停的觸發,知讀取取完畢
console.log(data) // 讀取10個數
rs.pause(); //暫停方法 表示暫停讀取,暫停data事件觸發
});
// 希望過一段時間繼續讀取
/*setTimeout(function(){
rs.resume();//恢復data事件觸發,繼續讀取,變為流動模式,但又會觸發pause,所以這裡可以使用setInterval,每隔一段時間執行一次
},1000);*/
setInterval(function(){
rs.resume();//恢復觸發data
},1000);
// 讀完以後,告知流結束,該事件會在讀完資料後被觸發
rs.on('end',function(){
console.log('end')
})
複製程式碼
以上實現的結果是;
檔案開啟
hel
low
orl
d
end
檔案關閉
//注:我檔案的資料是helloworld!
複製程式碼
根據上面的案例,簡單實現一個可讀流的庫(功能尚未完善):
// 可讀流的實現
// 可讀流是基於事件的,所以先把事件的模組先引進來
let EventEmitter = require('events');
let fs = require('fs');
// 實現一個createReadStream的類,類繼承EventEmitter
class RreadStream extends EventEmitter {
// 類需要路徑 物件 兩個引數
constructor(path,options){
// 繼承了必須幹一件事
super();//固定的寫法
this.path = path;
this.flags = options.flags || 'r';//如果沒傳,就給預設引數
this.autoClose = options.autoClose || true;//預設true
this.highWaterMark = options.highWaterMark|| 64*1024;//預設是64k
this.start = options.start||0;//預設開始是0
this.end = options.end;
this.encoding = options.encoding || null;//預設null
// 預設情況下,要先開啟檔案開始讀,先寫一個開啟檔案的方法
this.open();//開啟檔案 目的是為了獲取fd--檔案描述符,有了這個描述符,我們才能去讀取內容
// 是否監聽了data事件,如果監聽了,就變成流動模式
this.flowing = null; // null就是暫停模式
// 要建立一個buffer 這個buffer就是要一次讀多少
this.buffer = Buffer.alloc(this.highWaterMark);
this.pos = this.start; // pos 表示讀取的位置 可變 start不變的
// 如果有新事件繫結了,就會觸發newListener
this.on('newListener',(eventName,callback)=>{
if(eventName === 'data'){//如果事件名==data
// 相當於使用者監聽了data事件
this.flowing = true;//變成流動模式
// 監聽了 就去讀
this.read(); // 讀內容了,需要實現一個read方法
}
})
}
open(){
fs.open(this.path,this.flags,(err,fd)=>{//路徑,讀(開啟檔案的目的),回撥函式(箭頭函式防止this混亂),err表示讀檔案出錯了,fs表示檔案描述符
if(err){
this.emit('error',err);//檔案出錯,觸發error事件
if(this.autoClose){ // 是否自動關閉--防止檔案出錯,不能訪問
// 如果檔案可以自動關閉,那檔案關掉再觸發
this.destroy();//銷燬掉,需要實現一個destroy方法
}
return;//報錯就不往下走
}
this.fd = fd; // 如果檔案沒有出錯,就把檔案掛在當前的例項上 即儲存檔案描述符
this.emit('open'); // 檔案開啟成功了,監聽open
});
};
// destroy方法
destroy(){
// 先判斷有沒有fd描述符 有則關閉檔案 觸發close事件
if(typeof this.fd ==='number'){//有的話一定是number型別,true說明開啟過要銷燬
fs.close(this.fd,()=>{//把檔案先關掉,再銷燬,觸發close
this.emit('close');
});
return;
}
this.emit('close'); // 銷燬 , 需實現一個close方法
};
resume(){//恢復觸發data
this.flowing = true;
this.read();
}
pause(){//暫停方法 表示暫停讀取,暫停data事件觸發
this.flowing = false;
}
read(){
// 先判斷此時檔案有沒開啟
if(typeof this.fd !== 'number'){//如果檔案沒開啟 ,觸發一次open事件
// 檔案開啟 會觸發open事件,觸發事件後再執行read,此時fd肯定有了
return this.once('open',()=>this.read());//open走一次,就觸發一次,之後不再走了,因為已經有檔案了。
// 當觸發open,事件已經被觸發了,當觸發的時候,會走對應的回撥函式
}
// 此時有fd了
// 每次讀多少個。如果沒有end,隨便讀,讀多少個都可以。如果有highWaterMark,可能讀到某一個就停住了
let howMuchToRead = this.end?Math.min(this.highWaterMark,this.end-this.pos+1):this.highWaterMark;//讀的時候,用結尾的進去當前位置
// 比方說:想讀4個,但寫的是3,每次讀3個
// 第一次讀123 下一次讀一個4
fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
// 讀到了多少個 累加
if(bytesRead>0){
this.pos+= bytesRead;
// 判斷有沒傳encoding,如果傳了,轉成字串的形式,如果沒傳,說明預設是null
// this.buffer.slice(0,bytesRead)由buffer第0個開始就截,讀一個就取一個,讀兩個就取兩個,bytesRead是真實讀到的位置
let data = this.encoding?this.buffer.slice(0,bytesRead).toString(this.encoding):this.buffer.slice(0,bytesRead);
this.emit('data',data);
// 當讀取的位置 大於了末尾 就是讀取完畢了
if(this.pos > this.end){
this.emit('end');
this.destroy();
}
if(this.flowing) { // 流動模式繼續觸發
this.read();
}
}else{
this.emit('end');
this.destroy();
}
});
}
}
module.exports = RreadStream
複製程式碼
參考: Node.js文件