Node中的一股清流,比流川楓還要櫻花木道

我是家碧發表於2019-02-12

導讀:由於事件流是基於EventEmitter例項,並且也是通過fs模組來建立的,所以在學習事件流之前,最好先了解一下發布訂閱和fs檔案的操作,才可以對原始碼有更好的理解。

為什麼需要流

在node中讀取檔案的方式有來兩種,一個是利用fs模組,一個是利用流來讀取。如果讀取小檔案,我們可以使用fs讀取,fs讀取檔案的時候,是將檔案一次性讀取到本地記憶體。而如果讀取一個大檔案,一次性讀取會佔用大量記憶體,效率很低,這個時候需要用流來讀取。流是將資料分割段,一段一段的讀取,效率很高。

流的特點

流也是操作檔案的一種方式,流的特點有序(有順序的,順序不能打亂),有方向的

對檔案操作用的也是fs模組,可以用fs模組的方法來建立流

所有的流都是 EventEmitter 的例項。也就是流具有事件的能力,可以通過發射事件來反饋流的狀態。這樣我們就可以註冊監聽流的事件,來達到我們的目的。也就是我們訂閱了流的事件,這個事件觸發時,流會通知我,然後我就可以做相應的操作了。

流的分類

Readable Stream :可讀資料流

Writeable Stream :可寫資料流

Duplex Stream :雙向資料流,可以同時讀和寫

Transform Stream: 轉換資料流,可讀可寫,同時可以轉換(處理)資料

這裡我們介紹兩種:可讀流,可寫流

可讀流

可讀流的兩種模式

可讀流有兩種模式之一:flowing 和 paused 。

在 flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。

在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。

兩種模式之間的轉換

所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式:

  • 監聽 `data` 事件。

  • 呼叫 stream.resume() 方法。

  • 呼叫 stream.pipe() 方法將資料傳送到 Writable。

可讀流可以通過下面途徑切換到 paused 模式:

  • 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。

  • 如果存在管道目標,可以通過取消 `data` 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。

注意:如果可讀流切換到流動模式,並且沒有消費者處理流中的資料,這些資料將會丟失。

常用的事件

  1. `open`事件
  2. `close`事件
  3. `error`事件
  4. `data`事件 – 資料正在傳遞時,觸發該事件(以chunk資料塊為物件)
  5. `end`事件 – 資料傳遞完成後,會觸發該事件。

常用的方法

  1. rs.pause()暫停
  2. rs.resume()恢復

所有這些事件都可以在官方API文件中找到例子。我們可以監聽流的這些事件,來完成相應操作。通過以下程式碼來說明:

1.txt檔案,內容如下(這是我們要操作的檔案)

1234567890
複製程式碼

js檔案:建立可讀流和常用方法

let fs = require(`fs`);
let path = require(`path`);
// let ReadStream = require(`./ReadStream`);

// 1、建立可讀流(返回一個可讀流物件)  默是非流動模式,不會讀取資料
// 讀取的時候預設讀  預設64k,encoding 讀取預設都是buffer
let rs = fs.createReadStream(path.join(__dirname,`1.txt`),{
// let rs = new ReadStream(`./2.txt`,{
    flags:`r`, // 檔案的操作是讀取操作
    encoding:`utf8`,// 預設是null  null代表的是buffer
    autoClose:true,// 讀取完畢後自動關閉
    highWaterMark:3,// 預設是64k 64*1024b
    start:0,
    end:8 // 包前又包後 從0到8
});


// 2、可讀流常用方法
// 設定讀取的編碼格式 等價於encoding:`utf8`(建立可讀流不寫配置時,需要加上)
rs.setEncoding(`utf8`);
// 操作的是一個檔案,首先需要open開啟這個檔案
rs.on(`open`,function(){
    console.log(`檔案開啟了`);
});
// 檔案關閉觸發close事件
rs.on(`close`,function(){
    console.log(`關閉`);
});
// 出現錯誤
rs.on(`error`,function(){
    console.log(err);
});
// 預設情況下是非流動模式,通過監聽 `data`事件,使暫停模式變成流動模式,流動模式會
// 瘋狂的觸發data事件,直到讀取完畢
rs.on(`data`,function(data){
    console.log(data);
});
// 讀完後觸發end事件
rs.on(`end`,function(){
    console.log(`end`);
});
複製程式碼
// 輸出
檔案開啟了
123
456
789
end
關閉
複製程式碼

在以上的程式碼基礎上新增暫停,恢復事件方法

rs.on(`open`,function(){
    console.log(`檔案開啟了`);
});
rs.on(`close`,function(){
    console.log(`關閉`);
});
rs.on(`error`,function(){
    console.log(err);
});
rs.on(`data`,function(data){
    console.log(data);
    rs.pause(); // 暫停方法 表示暫停讀取,暫停data事件觸發
});
setTimeout(function(){
    rs.resume(); // 恢復data事件的觸發,繼續讀取, 變為流動模式
},3000);
rs.on(`end`,function(){
    console.log(`end`);
});
複製程式碼

這裡輸出123後會暫停三秒,等事件再次變為流動模式後,才會輸出456然後暫停,這裡並沒有將檔案讀取完畢,所以不會觸發end和close事件。

// 輸出 
檔案開啟了
123
456
複製程式碼

將setTimeout改成setInterval,每隔三秒恢復流動模式,輸出值,直到檔案讀取完畢。

// 輸出 
檔案開啟了
123
456
789
end
關閉
複製程式碼

實現一個可讀流

// ReadStream.js
let fs = require(`fs`); // 引入fs核心模組
let EventEmitter = require(`events`); // 需要依賴事件發射

// 1 建立例項繼承EventEmitter
class ReadStream extends EventEmitter{
    constructor(path,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; // null表示沒傳遞
        this.encoding = options.encoding || null;
        this.flags = options.flags || `r`;


        // 一些引數的問題
        // 6、非流動模式  null是暫停模式
        this.flowing = null; 
        // 7、看是否監聽了data事件,如果監聽了  就要變成流動模式(同步)
        this.on(`newListener`,(eventName,callback)=>{
            if(eventName === `data`){
                // 相當於使用者監聽了data事件
                this.flowing = true;
                // 監聽了 就去讀
                this.read(); // 去讀內容了
            }
        })


        // 9、要建立一個buffer,把內容讀到這個buffer裡,讀完以後再發射出去,這個buffer就是要
        //  一次讀highWaterMark個  
        this.buffer = Buffer.alloc(this.highWaterMark);
        //  9、pos讀取到的位置是可變的,start是不變的
        this.pos = this.start;

        //3、 只要讀流就要先開啟檔案,開啟檔案的目的是為了獲取檔案描述符fd,有了這描述符才
        //  能去讀取內容 
        //  this指的是例項,這是一個例項上的方法  open完了需要去監聽data事件才會觸發流動模式  
        this.open();
    }


    // 8、open是非同步的,newListener是同步的,read同步,同步的先執行,所以第一次呼叫read方法時還
    // 沒有獲取fd  所以不能直接讀
    read(){ 
        // console.log(this.fd) // 先列印的undefind  再去觸發的open
        if(typeof this.fd !== `number`){ // 通過fd判斷此時檔案還沒開啟
            return this.once(`open`,() => this.read()); 
            // 當檔案真正開啟後會觸發open事件,此時fd肯定拿到了,拿到以後再去執行read方法
            //(見10)
        }
        
        // 10、此時有fd了  開始讀取檔案  第二個引數(見9) 
        // buffer裡應該填多少? 計算howMuchToRead  // 123 4
        let howMuchToRead = this.end?Math.min(this.highWaterMark,this.end-this.pos+1)
        :this.highWaterMark;
        fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
            if(bytesRead>0){
                // 讀到了多少個  累加
                this.pos += bytesRead;
                // 這裡的this.buffer長度是3  需要擷取真實讀到的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();
            }
        });
    }


    // 12 恢復
    resume(){
        this.flowing = true;
        this.read();
    }
    // 11 暫停
    pause(){
        this.flowing = false
    }


    // 5、銷燬關閉檔案
    destroy(){
        // 通過fd判斷檔案是否開啟過,開啟了就關閉檔案,觸發close事件
        if(typeof this.fd === `number`){
            fs.close(this.fd,()=>{ 
                this.emit(`close`);
            });
            return
        }
        this.emit(`close`);
    }


    // 4、 開啟檔案
    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事件
                return;
            }
            this.fd = fd; // 沒有錯誤,將fd檔案描述符掛載到當前例項上
            this.emit(`open`,this.fd);// 檔案開啟成功後監聽open事件,觸發檔案開啟的方法  
        });
    }
}


// 2 
module.exports = ReadStream;
複製程式碼

可寫流

特點

可寫流有快取區的概念

第一次寫入是真的向檔案裡寫,第二次在寫入的時候是放到了快取區裡,過一會寫完了,會去清空快取區

寫入時會返回一個boolean型別,返回為false時不要再寫入了

當記憶體和正在寫入法人內容消耗完後 會觸發一個事件 drain

常用的方法

1,write(chunk[,encoding] [,callback])方法可以把資料寫入流中。

其中,chunk是待寫入的資料,是Buffer或String物件。這個引數是必須的,其它引數都是可選的。如果chunk是String物件,encoding可以用來指定字串的編碼格式,write會根據編碼格式將chunk解碼成位元組流再來寫入。callback是資料完全重新整理到流中時會執行的回撥函式。

write方法返回布林值,表示的並不是是否寫入,表示的是能否繼續寫,但是返回false也不會丟失,就是會把內容放到記憶體中

// 程式碼一
let fs = require(`fs`);
// 建立可寫流
let ws = fs.createWriteStream(`./1.txt`,{
    flags:`w`, // 預設檔案不存在會建立
    highWaterMark:3, // 設定當前快取區的大小,每次寫3個  預設寫是16k,讀是64k
    encoding:`utf8`,// 檔案裡存放的都是二進位制
    start:0, // 從哪開始寫
    autoClose:true, // 自動關閉
    mode:0o666,// 可讀可寫
});

// flag代表是否能繼續寫 寫的時候有個快取區的概念,先寫到快取區裡去,快取區沒滿是true,快取區滿
// 了就是flase,但是返回false也不會丟失,就是會把內容放到記憶體中
let flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
複製程式碼
// 輸出
true
true
false
複製程式碼

寫出的內容輸出到1.txt中(可寫流沒有會自動建立,有內容會清空,1.txt是自動建立的)

// 1.txt 輸出
111
複製程式碼

注意:最後一次當快取區滿了返回false時,資料並沒有丟失,最後還是輸出的三次=>111

2,end([chunk] [,encoding][,callback])方法可以用來結束一個可寫流。

它的三個引數都是可選的。chunk和encoding的含義與write方法類似。callback是一個可選的回撥,當你提供它時,它會被關聯到Writable的finish事件上,這樣當finish事件發射時它就會被呼叫。

當寫完觸發end事件後,就不能再繼續寫了

// 接著程式碼一的
ws.end(`ok`); // 這個ok也會寫到1.txt中去  當寫完後  就不能再繼續寫了
// ws.write(`123`); // 報錯  write after  end
複製程式碼
// 1.txt 輸出
111ok
複製程式碼

常用的事件

1、drain事件:抽乾方法。只有當highWaterMark快取區填滿,滿了後被清空了才會觸發drain。(當可讀流配合可寫流可以用drain事件寫一個pipe方法)

let fs = require(`fs`);
// 建立可寫流
let ws = fs.createWriteStream(`./1.txt`,{
    flags:`w`, // 預設檔案不存在會建立
    highWaterMark:3, // 設定當前快取區的大小,每次寫3個  預設寫是16k,讀是64k
    encoding:`utf8`,// 檔案裡存放的都是二進位制
    start:0, // 從哪開始寫
    autoClose:true, // 自動關閉
    mode:0o666,// 可讀可寫
});


let flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);
flag = ws.write(1+``,`utf8`,()=>{}); // 非同步的方法
console.log(flag);


ws.on(`drain`,function(){
    console.log(`drain`)
})
複製程式碼

這裡快取區highWaterMark大小是3個位元組,所以第三個flag時快取區已經滿了,所以從第三個flag開始都是false,但是資料並沒有丟失,即1.txt輸出四個1,並且觸發抽乾事件。如果這裡的write方法只寫入了兩次,快取區並沒有滿,就不會觸發這個抽乾事件。

// 輸出
true
true
false
false
drain
複製程式碼
// 1.txt 輸出
1111
複製程式碼

注意這裡每個write方法裡的數字1只佔用1個位元組,如果是11就2個位元組,一個數字佔用1個位元組,如果是一個漢字就是3個位元組。 所以如果第一個write方法寫入三個1,那第一個flag就會是false

寫個抽乾例子

let fs = require(`fs`);
// 建立可寫流
let ws = fs.createWriteStream(`./1.txt`,{
// let ws = new WS(`./2.txt`,{
    flags:`w`, // 預設檔案不存在會建立
    highWaterMark:3, // 設定當前快取區的大小,每次寫3個  預設寫是16k,讀是64k
    encoding:`utf8`,// 檔案裡存放的都是二進位制
    start:0, // 從哪開始寫
    autoClose:true, // 自動關閉
    mode:0o666,// 可讀可寫
});

// 寫內容的時候  必須是字串或者buffer
for(var i = 0;i < 9;i++){
    let flag = ws.write(i+``); // 一次寫一個字元
    console.log(flag);
}
複製程式碼

這裡highWaterMark快取大小是3位元組,所以第三次寫入時就滿了,之後都返回false

// 輸出
true
true
false
false
false
false
false
false
false
複製程式碼
// 1.txt 輸出
012345678
複製程式碼

當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 `drain` 事件就會被觸發,程式碼如下:

// let WS = require(`./WriteStreamS`);
let fs = require(`fs`);
// 建立可寫流
let ws = fs.createWriteStream(`./1.txt`,{
// let ws = new WS(`./2.txt`,{
    flags:`w`, // 預設檔案不存在會建立
    highWaterMark:3, // 設定當前快取區的大小,每次寫3個  預設寫是16k,讀是64k
    encoding:`utf8`,// 檔案裡存放的都是二進位制
    start:0, // 從哪開始寫
    autoClose:true, // 自動關閉
    mode:0o666,// 可讀可寫
});


let i = 9;
function write(){
    let flag = true;
    while (flag && i > 0) {
        // 當返回的是false時 flag識別符號為false  就不寫了
         flag = ws.write(--i + ``,`utf8`,()=>{console.log(`ok`)});   
        console.log(i+""+flag)
    }
}
write();
// 以上程式碼輸出的是truetruefalse,當highWaterMark填滿了就不會繼續輸出,1.txt輸出的是876 


// drain只有當快取區充滿後  並且被消費後才會觸發
ws.on(`drain`,function () {
    console.log(`抽乾`);
    write();
})
複製程式碼

寫完一次(一次快取區是個highWaterMark 3個位元組),清空快取區,再繼續寫

// 輸出
8true
7true
6false
抽乾
5true
4true
3false
抽乾
2true
1true
0false
抽乾
複製程式碼
// 1.txt 輸出
876543210
複製程式碼

2、finish事件:在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, `finish` 事件將被觸發。

實現一個可寫流

// WriteStreamS.js
let fs = require(`fs`);
let EventEmitter = require(`events`);

// 1、建立例項繼承EventEmitter
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 // 讀64 寫16

       

        // 3、寫檔案的時候  需要的引數有哪些
        // 3-1、可寫流有快取區,第一次寫是真的往檔案裡寫,之後如果正在寫入檔案的時候,內容要
        // 寫入到快取區中,寫一點放快取裡一點,寫完之後再把快取裡的取出來再去寫,這個快取區
        // 在原始碼中是一個連結串列,這裡我們為了方便,我們使用陣列去存內容
        // 快取用簡單的陣列來模擬一下
        this.cache = [];
        // 3-2、標識  是否正在寫入  預設第一次就不是正在寫入
        this.writing = false; 
        // 3-3、是否滿足觸發drain事件(只有當快取區滿了被清空後觸發)
        this.needDrain = false;
        // 3-4、維護一個變數  表示快取的長度
        this.len = 0;
         // 3-5、記錄寫入的位置,一次寫3個
         this.pos = 0;
         

        // 4、 寫流之前要先開啟(開啟檔案目的就是為了獲取fd描述符)
        this.open(); // fd 非同步的  觸發一個open事件  當觸發open事件後fd肯定就存在了
        
    }
    
    
    // 9、遞迴清空快取區  [7,6,5,4,3,2,1]
    clearBuffer(){
        let buffer = this.cache.shift(); // 從陣列第一個開始取出來刪除
        if(buffer){ // 快取裡有(陣列裡有值)
            this._write(buffer.chunk,buffer.encoding,()=>{
                buffer.callback();
                this.clearBuffer();
            });
        }else{ // 快取裡沒有了
            if(this.needDrain){ // 是否需要觸發drain事件  需要就發射drain事件
                this.writing = false;// 告訴下次直接寫就可以了  不需要寫到記憶體中了
                this.needDrain = false;
                this.emit(`drain`)
            }
        }
    }


    //  8、把chunk往檔案裡寫
    _write(chunk,encoding,clearBuffer){ 
        // 因為write方法是同步呼叫,open非同步,先調write時,此時fd還沒有獲取到(fd是從3開始的
        // 數字)所以先開啟open獲取到fd後再執行write操作
        if(typeof this.fd != `number`){ // fd不是數字說明不存在,觸發open將_write方法存起來
            return this.once(`open`, () => this._write(chunk,encoding,clearBuffer));
        }
        fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
            this.pos += byteWritten;
            this.len -= byteWritten; // 每次寫入後就要在記憶體中減少一下
            clearBuffer();// 第一次就寫完了  去清空快取區內容
        })
    }
    
    
    // 7、客戶呼叫的是write方法去寫入內容
    write(chunk,encoding=this.encoding,callback=()=>{}){
        // 要判斷  chunk必須是buffer或者字串  為了統一,如果傳遞的是字串也要轉車buffer
        // 這裡用encoding編碼把chunk轉換成buffer
        chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding);
        this.len += chunk.length; // 維護快取的長度
        let ret = this.len < this.highWaterMark;
        if(!ret){
            this.needDrain = true; // 快取區滿了 !ret返回true  觸發drain事件
        }
        // 判斷是否正在寫入  如果是正在寫入  就寫入到快取區中  
        if(this.writing){ 
            this.cache.push({
                chunk,
                encoding,
                callback
            }); // [7,6,5,4,3,2,1]
        }else{ // 第一次往檔案裡寫
            this.writing = true;
            this._write(chunk,encoding,()=>{
                callback();
                this.clearBuffer();
            }); // 專門實現寫的方法,第一次寫入8  這裡的回撥用來清空快取區
        }
        return ret; //能不能繼續寫了,false表示下次寫的時候就要佔用更多記憶體了
    }



    // 6、銷燬
    destroy(){
        if(typeof this.fd !=`number`){
            this.emit(`close`);
        }else{
            fs.close(this.fd,()=>{
                this.emit(`close`); 
            })
        }
    }
    
    
    // 5、開啟檔案
    open(){
        fs.open(this.path,this.flags,this.mode,(err,fd)=>{
            if(err){
                this.emit(`error`,err);
                if(this.autoClose){
                    this.destroy(); // 如果自動關閉就銷燬檔案描述符
                }
                return;
            }
            this.fd = fd;
            this.emit(`open`,this.fd);
        });
    }
}


// 2、匯出這個例項
module.exports = WriteStream;
複製程式碼

整個思路流程

原始碼中一開始就執行了一個open方法,這個open方法主要是為了開啟檔案,獲取到描述符fd,有描述符後才可以真正的去寫去讀,因為使用者不會等你開啟檔案後再去寫,他會直接開始呼叫write方法去寫,所以當使用者調write方法時,我們通過once觸發了open事件。(注意:在open方法中,釋出了一個open事件,後面通過on/once監聽這個事件去觸發open()方法並通過回撥函式去處理業務邏輯。這個是釋出訂閱中的知識點。)

write方法中有幾個引數:chunk,encoding,callback。我們要看一看chunk是不是buffer,如果是buffer我們就用buffer,因為寫的時候要寫buffer,不是buffer, 就轉換成buffer。通過this.len算出寫的內容長度,用這個長度和highWaterMark比,如果和highWaterMark相等了,就說明當前快取區滿了,ret就為false,(ret為false時就是快取區寫滿了,ret為trun時就是快取區還沒寫滿)這時候write方法將會通過ret返回一個布林型別。只有當快取區滿了時,也就是ret為false時,並且滿了被清空後(這裡通過clearBuffer去清空的),會去觸發抽乾事件。第一次開始寫就把this.writing變成true,表示正在寫入,之後開始寫的就往快取區裡寫(這裡快取區是陣列模擬的),第一次寫入時,我們通過_write這個方法去真正的往檔案裡去寫,當這個檔案被寫入以後,再去取快取
區裡的第一項,這裡通過clearBuffer方法去取陣列的第一個值往檔案裡去寫(逐步清空這個資料),當陣列裡第一個寫完之後就再去呼叫這個clearBuffer方法再去清空快取區,直
到快取區裡的值被清空完為止。當快取區裡值清空完了需要判斷是否要觸發抽乾事件,要觸發的話就通過emit釋出抽乾事件,使用者就可以用on監聽並通過回撥函式處理業務邏輯

這個_write方法每次寫的時候,我們就要把快取區大小減少(陣列長度減少),寫入的位置增加,並且把this.writing變成false(表示正在寫入),每次寫完後
,通過clearBuffer清空快取區。

結論:整個思路是圍繞第一次是真正往檔案裡去寫,寫之前就要通過open拿到fd,之後再寫入的資料就放到快取區裡,當快取區滿了被清空後去觸發抽乾事件。所以我們是寫滿快取區,清空快取區,觸發抽乾事件,這樣迴圈,直到資料被寫完畢。這裡需要注意的是,如果最後一次寫入的內容沒有寫滿快取區,最後一次就不會觸發抽乾事件。抽乾事件的觸發條件有兩個:highWaterMark快取區填滿,滿了後被清空了才會觸發

相關文章