淺析node中流應用(一) 可讀流(fs.createReadStream)

言sir發表於2018-07-05

淺析node中流應用(一)  可讀流(fs.createReadStream)

為什麼要需要流?

  • 當我們學習新知識的時候,首先我們知道為什麼要學習,那我們為什麼要學習流?因為在在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官網詳細介紹
    http://nodejs.cn/api/fs.html#fs_fs_createreadstream_path_options

監聽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
});
複製程式碼

淺析node中流應用(一)  可讀流(fs.createReadStream)

監聽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講完了,迫不及待的寫個完整的案例來體驗下,說幹就幹

淺析node中流應用(一)  可讀流(fs.createReadStream)

手寫可讀流

一、準備工作,構建可讀流建構函式

  • 記住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')已經實現了,我們來測試下吧

淺析node中流應用(一)  可讀流(fs.createReadStream)

四、實現主要的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();
    }
複製程式碼

終於大功告成,寫的對不對呢,趕緊測試下吧,期待的搓手手

淺析node中流應用(一)  可讀流(fs.createReadStream)

end

  • 我們已經實現了可讀流實現,後續還會有可寫流實現。api雖然枯燥,希望大家還是多寫寫原始碼
  • 對原始碼感興趣,我把原始碼放在github上 ,供大家參考

相關文章