淺析node中流應用(二) 可寫流(fs.createWriteStream)

言sir發表於2018-07-08

淺析node中流應用(二) 可寫流(fs.createWriteStream)

上篇文章寫了可讀流的用法和原始碼實現,可以先去看下,其中有相似的地方,重複的地方就不多做介紹了,就直接寫用法。有一點值得一提就是可寫流有快取區的概念

可寫流的用法

建立可寫流

let fs = require('fs');
let ws = fs.createWriteStream('2.txt', {
    flags: 'w',         // 檔案的操作, 'w'寫入檔案,不存在則建立
    mode: 0o666,
    autoClose: true,
    highWaterMark: 3,   // 預設寫是16*1024
    encoding: 'utf8'
});

複製程式碼

ws.write()方法和drain事件

淺析node中流應用(二) 可寫流(fs.createWriteStream)

  • 每次寫入後會有一個標識 flag,當寫入的內容的長度超過highWaterMark就會返回false
  • true表示可以繼續寫入,如果返回false,表示快取區滿了,我們應當停止讀取資料以避免消耗過多記憶體。
  • 快取區滿後,檔案寫入一直在進行,不一會兒會把快取區的內容全部寫入,快取區處於清空狀態,這時會觸發可寫流的‘drain’事件

不太明白?沒關係,我們看下面這個例子幫助我們理解

let fs = require("fs");
let ws = fs.createWriteStream('1.txt',{
    flags:'w',
    encoding:'utf8',
    start:0,
    highWaterMark:3
});
let i =9;
function write() {
    let flag = true;
    while (flag && i>=0){
      flag =   ws.write(i-- +'');//往1.txt寫入9876543210
        console.log(flag);
    }
}
ws.on('drain',()=>{ //快取區充滿並被寫入完成,處於清空狀態時觸發
    console.log("幹了");
    write(); //當快取區清空後我們在繼續寫
})
write(); //第一次呼叫write方法


複製程式碼

淺析node中流應用(二) 可寫流(fs.createWriteStream)

看到這裡,我們應該基本明白用法,下面我們開始寫原始碼實現,鑑於與可讀流有很多相似寫法上篇文章已詳細寫過,重複的就不多說了,說幹就幹

可寫流實現原理

1、宣告WriteStream的建構函式(準備工作)

let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
    constructor(path,options={}){
        super();
        this.path = path;
        this.flags = options.flags || 'w';
        this.encoding = options.encoding || 'utf8';
        this.start = options.start || 0;
        this.pos = this.start;
        this.mode = options.mode || 0o666;
        this.autoClose = options.autoClose || true;
        this.highWaterMark = options.highWaterMark || 16 * 1024;

        //第一次寫入是真的往檔案裡寫
        this.writing = false; //預設第一次不是正在寫入

        // 可寫流  要有一個快取區,當正在寫入檔案時,內容要寫入到快取區
        this.cache = [];

        // 記錄快取區大小
        this.len =0;

        // 是否觸發drain事件
        this.needDrain = false;


        this.open(); //目的是拿到fd 非同步,觸發一個open事件後fd肯定存在啦
    }
  }
複製程式碼
  • 把變數宣告好,下面用到的時候就知道怎麼回事了
  • 值得主要的是,我們第一次往檔案裡寫入的時候是真的往檔案寫,下次要寫的內容先放到快取區,寫入完成後從快取區去取內容

2、實現open方法和destroy方法

  • 這兩個方法與可讀流一樣,不做介紹,直接上程式碼
open(){
        fs.open(this.path,this.flags,this.mode,(err,fd)=>{
            if(err){
                this.emit('error', err); //開啟檔案發生錯誤,釋出error事件
                this.emit('error');
                if (this.autoClose) { //如果需要自動關閉我再去銷燬fd
                    this.destroy(); //關閉檔案(觸發關閉事件)
                }
                return;
            }
            this.fd = fd; //儲存檔案描述符
            this.emit('open', this.fd) //觸發檔案open方法
        })
    }
    destroy() {
        if (typeof this.fd != 'number') { //檔案未開啟,也要關閉檔案且觸發close事件
            return this.emit('close');
        }
        fs.close(this.fd, () => {  //如果檔案開啟過了 那就關閉檔案並且觸發close事件
            this.emit("close");
        })
    }
複製程式碼

3、實現write方法,與客戶端呼叫ws.write()方法對應

 write(chunk,encoding = this.encoding,callback){ //客戶呼叫的是write
        //chunk必須是buffer或者字串, 為了統一,如果傳遞的是字串也要轉成buffer
        chunk = Buffer.isBuffer(chunk)?chunk : Buffer.from(chunk,encoding);
        this.len +=chunk.length; //維護寫入的快取的長度
        let ret = this.len <this.highWaterMark; //一個標識 比較是否達成了快取區的大小
        this.needDrain = !ret; //是否需要觸發needDrain
        if(this.writing){ //預設為false上面定義的,判斷是否正在寫入  如果是正在寫入 就寫入到快取區中
            this.cache.push({chunk,encoding,callback})
        }else { //第一次寫
            this.writing = true;
            this._write(chunk,encoding,()=>this.clearBuffer()); //專門實現寫的方法

        }

        return ret; //能不能繼續寫了 false表示下次寫的時候就要佔用記憶體

    }
複製程式碼
  • _write()方法 重點, 我們專門用來實現真正的寫入方法

4 、實現重頭戲 _write()方法

 _write(chunk,encoding,clearBuffer){
        if(typeof this.fd!= 'number'){
            //因為write方法是同步呼叫,此時fd還沒有獲取到,所以等待獲取到再執行write
            return this.once('open',()=>this._write(chunk,encoding,clearBuffer));
        }
        //確保有fd
        fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
            this.pos +=byteWritten; //偏移量,預設為0
            this.len -=byteWritten; //每次寫入後就要在記憶體中減少下
            this.writing = false; //正在寫入就不要再寫拉,放快取區
            clearBuffer(); //清空快取區
            
        })
    }
複製程式碼

5、清空快取區

 let buffer = this.cache.shift(); //取快取區第一個內容
        if(buffer){ //快取裡有
            this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer())
        }else { //快取裡沒有了
            if(this.needDrain){ //需要觸發drain事件
                this.writing = false; //告訴下次直接寫就可以了  不需要寫到記憶體中
                this.needDrain = false;
                this.emit('drain');
            }
        }
複製程式碼
  • 分析下
  • 如果是正在寫入,就先把內容放到快取區裡,就是this.cache,預設[]
  • 給陣列裡存入一個物件,分別對應chunk, encoding, callback即(()=>this.clearBuffer())
  • 每一次寫完後都需要把cache(快取區)裡的內容清空掉
  • 當快取區cache陣列裡是空的時候就會觸發drain事件了

寫到這裡,基本就寫完了,原始碼有3000多行,這個只是簡單實現,看不太懂的時候就多寫幾遍(ps 我也寫了好多遍)。測試下看行不行吧

淺析node中流應用(二) 可寫流(fs.createWriteStream)

相關文章