為什麼要需要流?
- 當我們學習新知識的時候,首先我們知道為什麼要學習,那我們為什麼要學習流?因為在在node中讀取檔案的方式有來兩種,一個是利用fs模組,一個是利用流來讀取。如果讀取小檔案,我們可以使用fs讀取,fs讀取檔案的時候,是將檔案一次性讀取到本地記憶體。而如果讀取一個大檔案,一次性讀取會佔用大量記憶體,效率很低,這個時候需要用流來讀取。流是將資料分割段,一段一段的讀取,可以控制速率,效率很高,不會佔用太大的記憶體。gulp的task任務,檔案壓縮,和http中的請求和響應等功能的實現都是基於流來實現的。因此,系統學習下流還是很有必要的
可讀流用法(先把用法學會)
- node中讀是將內容讀取到記憶體中,而記憶體就是Buffer物件
- 流都是基於原生的fs操作檔案的方法來實現的,通過fs建立流。所有的 Stream 物件都是 EventEmitter 的例項。常用的事件有:
- open -開啟檔案
- data -當有資料可讀時觸發。
- error -在讀收和寫入過程中發生錯誤時觸發。
- close -關閉檔案
- end - 沒有更多的資料可讀時觸發
建立可讀流
- 統一下 1.txt中的內容 1234567890
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
highWaterMark:3, //檔案一次讀多少位元組,預設 64*1024
flags:'r', //預設 'r'
autoClose:true, //預設讀取完畢後自動關閉
start:0, //讀取檔案開始位置
end:3, //流是閉合區間 包含start也含end
encoding:'utf8' //預設null
});
複製程式碼
- 注意: 預設建立一個流 是非流動模式,預設不會讀取資料
- 具體引數說明,我們可以參考下node官網詳細介紹
監聽open事件
rs.on("open",()=>{
console.log("檔案開啟")
});
複製程式碼
監聽data事件
-
可讀流這種模式它預設情況下是非流動模式(暫停模式),它什麼也不做,就在這等著
-
監聽了data事件的話,就可以將非流動模式轉換為流動模式
-
流動模式會瘋狂的觸發data事件,直到讀取完畢
-
直接上程式碼
//1.txt中內容為1234567890
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
highWaterMark:3, //檔案一次讀多少位元組,預設 64*1024
flags:'r', //預設 'r'
autoClose:true, //預設讀取完畢後自動關閉
start:0, //讀取檔案開始位置
end:3, //流是閉合區間 包含start也含end
encoding:'utf8' //預設null
});
rs.on("open",()=>{
console.log("檔案開啟")
});
//瘋狂觸發data事件 直到讀取完畢
rs.on('data',(data)=>{
console.log(data); //共讀4個位元組,但是highWaterMark為3,所以觸發2次data事件,分別列印123 4
});
複製程式碼
監聽err/end/close事件
rs.on("err",()=>{
console.log("發生錯誤")
});
rs.on('end',()=>{ //檔案讀取完畢後觸發
console.log("讀取完畢");
});
rs.on("close",()=>{ //最後檔案關閉觸發
console.log("關閉")
});
複製程式碼
不要急,最後把方法介紹完統一寫個例子,大家一看便一目了之
最後介紹兩個方法就大功告成啦
- rs.pause() 暫停讀取,會暫停data事件的觸發,將流動模式轉變非流動模式
- rs.resume()恢復data事件,繼續讀取,變為流動模式
終於把可讀流的所有API講完了,迫不及待的寫個完整的案例來體驗下,說幹就幹
- 用法講完了,可以開始寫寫原始碼實現了,一共不到100行,來慢慢感受下,還記得剛開始的時候說的所有的 Stream 物件都是 EventEmitter 的例項。如果對釋出訂閱模式中EventEmitter還不瞭解,可以讀下上篇文章釋出訂閱模式還不會??戳這裡,50行核心程式碼,手把手教你學會
手寫可讀流
一、準備工作,構建可讀流建構函式
- 記住Stream 物件都是 EventEmitter 的例項,內部是通過釋出訂閱模式實現的。直接貼程式碼
let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter { //建立可讀流類,繼承 EventEmitter
constructor(path, options = {}) { //options預設空物件
super();
this.path = path;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.pos = this.start; //pos會隨著讀取的位置改變
this.end = options.end || null;
this.encoding = options.encoding || null;
this.flags = options.flags || 'r';
this.flowing = null; //非流動模式
//宣告一個buffer表示都出來的資料
this.buffer = Buffer.alloc(this.highWaterMark);
this.open(); //開啟檔案 fd
}
複製程式碼
- 其實只是賦值了很多預設值,沒有什麼難點,接下來就要寫this.open()方法,即開啟檔案
二、在ReadStream原型中寫open方法
- 廢話不多說,直接上程式碼,程式碼中有詳細的程式碼解釋
//開啟檔案用
open() {
fs.open(this.path, this.flags, (err, fd) => { //fd標識的就是當前this.path這個檔案,從3開始(number型別)
if (err) {
if (this.autoClose) { //如果需要自動關閉我再去銷燬fd
this.destroy(); //關閉檔案(觸發關閉事件)
}
this.emit('error', err); //開啟檔案發生錯誤,釋出error事件
}
this.fd = fd; //儲存檔案描述符
this.emit('open', this.fd) //觸發檔案open方法
})
}
複製程式碼
- 想下,開啟檔案我們做了兩件事,
- 1、如果發生錯誤,關閉檔案,同時發射 "error"事件
- 2、如果沒有錯誤,儲存fd,然後發射 "open"事件
- 先來實現下this.destroy()關閉檔案的方法
三、實現destroy()方法
destroy() {
if (typeof this.fd != 'number') { //檔案未開啟,也要關閉檔案且觸發close事件
return this.emit('close');
}
fs.close(this.fd, () => { //如果檔案開啟過了 那就關閉檔案並且觸發close事件
this.emit("close");
})
}
複製程式碼
- 這樣一來,rs.on('open')已經實現了,我們來測試下吧
四、實現主要的read方法真的讀檔案,於rs.on('data')方法對應
- 1、確保真的拿到fd(檔案描述符,預設3,number型別)
- 2、確保拿到fd後,對fs.read中howMuchToRead有一個繞的演算法,多舉幾個例子理解更好,如果對fs.read不瞭解,戳這裡,fs.read()方法介紹
- 3、非同步遞迴去讀檔案,讀完為止。
- 4、說了這麼多,直接幹。
read() {
//此時檔案還沒開啟
if (typeof this.fd != 'number') {
//當檔案真正開啟的時候 會觸發open事件,觸發事件後再執行read,此時fd 就有了
return this.once('open', () => this.read())
}
//此時有fd了 開始讀取檔案了
//this.pos是變數,開始時this.pos = this.start,在上面定義過了
//演算法有點繞,原始碼中是這樣實現的。舉個例子 end=3,pos=0,highWaterMark=3, howMuchToRead = 3, 1.txt內容1234 就會讀123 4
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) => {
// byteRead真實讀到的個數
this.pos += byteRead;
// this.buffer預設三個
let b = this.buffer.slice(0, byteRead);
//對讀到的b進行編碼
b = this.encoding ? b.toString(this.encoding) : b;
//把讀取到的buffer發射出去
this.emit('data', b);
if ((byteRead === this.highWaterMark) && this.flowing) {
return this.read();
}
//這裡沒有更多邏輯了
if (byteRead < this.highWaterMark) {
//沒有更多了
this.emit('end'); //讀取完畢
this.destroy(); //銷燬完畢
}
})
}
複製程式碼
大家會發現,此時我們還沒有監聽 rs.on('data')事件,來觸發read方法,此時我們需要修改下 第一步建立建構函式的程式碼
constructor(path, options = {}) {
//省略.... 程式碼和第一步一樣,下面是新新增
// 看是否監聽了data事件,如果監聽了就要變成流動模式
this.on('newListener', (eventName, callback) => {
if (eventName === 'data') {
//相當於使用者監聽了data事件
this.flowing = true;
// 監聽了就去讀
this.read(); //去讀內容
}
})
}
複製程式碼
如果能看到這裡,就基本大功告成,就只剩下pause和resume 暫停和恢復暫停方法。那就一寫到底
五、新增pause暫停 和resume恢復暫停方法
- 兩個方法非常簡單,就直接貼程式碼
pause() {
this.flowing = false;
}
resume() {
this.flowing = true;
//恢復暫停,在去無限讀
this.read();
}
複製程式碼
終於大功告成,寫的對不對呢,趕緊測試下吧,期待的搓手手
end
- 我們已經實現了可讀流實現,後續還會有可寫流實現。api雖然枯燥,希望大家還是多寫寫原始碼
- 對原始碼感興趣,我把原始碼放在github上 ,供大家參考