渴望力量嗎?少年!流的原理

ldzsl發表於2021-09-09

流(stream),看一個人流不流逼,就看你對流的理解了

學習本無底,前進莫徬徨

今天跟大家分享的是node.js中的流(stream)。它的作用大家應該都在平常使用node的時候看到過,比如:

  • gulp中的pipe就是流的一種方法,通過可寫流和可讀流的配合,達到不佔用多餘快取的一種讀寫方式。
  • express和koa中的res和req也是流,res是可寫流,req是可讀流,他們都是通過封裝node中的net模組的socket(雙工流,即可寫、可讀流)而來的。
  • 。。。

可能很多時候大家都知道怎麼用,但不瞭解它的原理,很尷尬,就像這樣

渴望力量嗎?少年!流的原理

何謂流?

  • 流是一組有序的,有起點和終點的位元組資料傳輸手段。
  • 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理。
  • 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
  • 流被分為Readable(可讀流)、Writable(可寫流)、Duplex(雙工流)、Transform(轉換流)

流中的是什麼?

  • 二進位制模式:每個分塊都是buffer、string物件。
  • 物件模式:流內部處理的是一系列普通物件。

可讀流

可讀流分為flowingpaused兩種模式

引數

  • path:讀取的檔案的路徑
  • option:
    • highWaterMark:水位線,一次可讀的位元組,一般預設是64k
    • flags:標識,開啟檔案要做的操作,預設是r
    • encoding:編碼,預設為buffer
    • start:開始讀取的索引位置
    • end:結束讀取的索引位置(包括結束位置)
    • autoClose:讀取完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')
//讀取的時候預設讀64k 
let rs = new ReadStream('./a.txt',{
  highWaterMark: 2,//一次讀的位元組 預設64k
  flags: 'r',      //標示 r為讀 w為寫
  autoClose: true, //預設讀取完畢後自動關閉
  start: 0,
  end: 5,          //流是閉合區間包start,也包end 預設是讀完
  encoding: 'utf8' //預設編碼是buffer
})
複製程式碼

方法

data:切換到流動模式,可以流出資料

rs.on('data', function (data) {
    console.log(data);
});
複製程式碼

open:流開啟檔案的時候會觸發此監聽

rs.on('open', function () {
    console.log('檔案被開啟');
});
複製程式碼

error:流出錯的時候,監聽錯誤資訊

rs.on('error', function (err) {
    console.log(err);
});
複製程式碼

end:流讀取完成,觸發end

rs.on('end', function (err) {
    console.log('讀取完成');
});
複製程式碼

close:關閉流,觸發

rs.on('close', function (err) {
    console.log('關閉');
});
複製程式碼

pause:暫停流(改變流的flowing,不讀取資料了);resume:恢復流(改變流的flowing,繼續讀取資料)

//流通過一次後,停止流動,過了2s後再動
rs.on('data', function (data) {
    rs.pause();
    console.log(data);
});
setTimeout(function () {
    rs.resume();
},2000);
複製程式碼

fs.read():可讀流底層呼叫的就是這個方法,最原生的讀方法

//fd檔案描述符,一般通過fs.open中獲取
//buffer是讀取後的資料放入的快取目標
//0,從buffer的0位置開始放入
//BUFFER_SIZE,每次放BUFFER_SIZE這麼長的長度
//index,每次從檔案的index的位置開始讀
//bytesRead,真實讀到的個數
fs.read(fd,buffer,0,BUFFER_SIZE,index,function(err,bytesRead){

})
複製程式碼

那讓我們自己來實現一個可愛的讀流吧!

渴望力量嗎?少年!流的原理

let fs = require('fs')
let EventEmitter = require('events')
class ReadStream extends EventEmitter{
  constructor(path,options = {}){
    super()
    this.path = path
    this.highWaterMark = options.highWaterMark || 64*1024
    this.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start     //會隨著讀取的位置改變
    this.autoClose = options.autoClose || true
    this.end = options.end || null
    //預設null就是buffer
    this.encoding = options.encoding || null

    //引數的問題
    this.flowing = null //非流動模式
    //建立個buffer用來儲存每次讀出來的資料
    this.buffer = Buffer.alloc(this.highWaterMark)
    //開啟這個檔案
    this.open()
    //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
    this.on('newListener',(type)=>{// 等待著他監聽data事件
      if(type === 'data'){
        this.flowing = true
        //開始讀取 客戶已經監聽的data事件
        this.read()
      }
    })
  }
  //預設第一次呼叫read方法時fd還沒獲取 所以不能直接讀
  read(){
    if(typeof this.fd != 'number'){
      //等待著觸發open事件後fd肯定拿到了 再去執行read方法
      return this.once('open',()=>{this.read()})
    }

    //每次讀的時候都要判斷一下下次讀幾個 如果沒有end就根據highWaterMark來(讀所有的) 如果有且大於highWaterMark就根據highWaterMark來 如果小於highWaterMark就根據end來
    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)=>{
      this.pos += byteRead
      let b = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead)
      this.emit('data',b)
      //如果讀取到的數量和highWaterMark一樣 說明還得繼續讀
      if((byteRead === this.highWaterMark)&&this.flowing){
        this.read()
      }
      if(byteRead < this.highWaterMark){
        this.emit('end')
        this.destory()
      }
    })
  }
  destory(){
    if(typeof this.fd != 'number'){
      return this.emit('close')
    }
    //如果檔案被開啟過 就關閉檔案並且觸發close事件
    fs.close(this.fd,()=>{
      this.emit('close')
    })
  }
  pause(){
    this.flowing = false
  }
  resume(){
    this.flowing = true
    this.read()
  }
  open(){
    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{
      //有可能fd這個檔案不存在 需要做處理
      if(err){
        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){
          //銷燬(關閉檔案,觸發關閉檔案事件)
          this.destory()
        }
        //如果有錯誤 就會觸發error事件
        this.emit('error',err)
        return
      }
      //儲存檔案描述符
      this.fd = fd
      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}
複製程式碼

Readable

這個方法是可讀流的一種暫停模式,他的模式可以參考為讀流是往水杯倒水的人,Readable是喝水的人,他們之間存在著一種聯絡,只要Readable喝掉一點水,讀流就會繼續往裡倒

Readable是什麼?

  • 他會在剛開始監聽Readable的時候就觸發流的,此時流就會讀取一次資料,之後流會監聽,如果有人讀過流(喝過水),並且減少,就會再去讀一次(倒點水)
  • 主要可以用來做行讀取器(LineReader)
let fs = require('fs')
let read = require('./ReadableStream')
let rs = fs.createReadStream('./a.txt', {
  //每次讀7個
  highWaterMark: 7
})
//如果讀流第一次全部讀下來並且小於highWaterMark,就會再讀一次(再觸發一次readable事件)
//如果rs.read()不加引數,一次性讀完,會從快取區再讀一次,為null
//如果readable每次都剛好讀完(即rs.read()的引數剛好和highWaterMark相等),就會一直觸發readable事件,如果最後不足他想喝的數,他就會先觸發一次null,最後把剩下的喝完
//一開始快取區為0的時候也會預設調一次readable事件
rs.on('readable', () => {
  let result = rs.read(2)
  console.log(result)
})
複製程式碼

實戰:行讀取器(平常我們的檔案可能有回車、換行,此時如果要每次想讀一行的資料,就得用到readable)

let EventEmitter = require('events')
//如果要將內容全部讀出就用on('data'),精確讀取就用on('readable')
class LineReader extends EventEmitter {
  constructor(path) {
    super()
    this.rs = fs.createReadStream(path)
    //回車符的十六進位制
    let RETURN = 0x0d
    //換行符的十六進位制
    let LINE = 0x0a
    let arr = []
    this.on('newListener', (type) => {
      if (type === 'newLine') {
        this.rs.on('readable', () => {
          let char
          //每次讀一個,當讀完的時候會返回null,終止迴圈
          while (char = this.rs.read(1)) {
            switch (char[0]) {
              case RETURN:
                break;
              //Mac下只有換行符,windows下是回車符和換行符,需要根據不同的轉換。因為我這裡是Mac
              case LINE:
                //如果是換行符就把陣列轉換為字串
                let r = Buffer.from(arr).toString('utf8')
                //把陣列清空
                arr.length = 0
                //觸發newLine事件,把得到的一行資料輸出
                this.emit('newLine', r)
                break;
              default:
                //如果不是換行符,就放入陣列中
                arr.push(char[0])
            }
          }
        })
      }
    })
    //以上只能取出之前的換行符前的程式碼,最後一行的後面沒有換行符,所以需要特殊處理。當讀流讀完需要觸發end事件時
    this.rs.on('end', () => {
      //取出最後一行資料,轉成字串
      let r = Buffer.from(arr).toString('utf8')
      arr.length = 0
      this.emit('newLine', r)
    })
  }
}

let lineReader = new LineReader('./a.txt')
lineReader.on('newLine', function (data) {
  console.log(data)
})
複製程式碼

那麼Readable到底是怎樣的存在呢?我們接下來實現他的原始碼,看看內部到底怎麼回事

let fs = require('fs')
let EventEmitter = require('events')
class ReadStream extends EventEmitter{
  constructor(path,options = {}){
    super()
    this.path = path
    this.highWaterMark = options.highWaterMark || 64*1024
    this.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start     //會隨著讀取的位置改變
    this.autoClose = options.autoClose || true
    this.end = options.end || null
    //預設null就是buffer
    this.encoding = options.encoding || null

    //引數的問題
    this.reading = false //非流動模式
    //建立個buffer用來儲存每次讀出來的資料
    this.buffers = []
    //快取區長度
    this.len = 0
    //是否要觸發readable事件
    this.emittedReadable = false
    //觸發open獲取檔案的fd識別符號
    this.open()
    //此方法預設同步呼叫 每次設定on監聽事件時都會呼叫之前所有的newListener事件
    this.on('newListener',(type)=>{// 等待著他監聽data事件
      if(type === 'readable'){
        //開始讀取 客戶已經監聽的data事件
        this.read()
      }
    })
  }
  //readable真正的原始碼中的方法,計算出和n最接近的2的冪次數
  computeNewHighWaterMark(n) {
    n--;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    n++;
    return n;
  }
  read(n){
    //當讀的數量大於水平線,會通過取2的冪次取比他大和最接近的數
    if(this.len < n){
      this.highWaterMark = this.computeNewHighWaterMark(n)
      //重新觸發readbale的callback,所以第一次會觸發null
      this.emittedReadable = true
      //重新讀新的水位線
      this._read()
    }
    //真正讀取到的
    let buffer = null
    //說明快取裡有這麼多,取出來
    if(n>0 && n<=this.len){
      //定義一個buffer
      buffer = Buffer.alloc(n)
      let buf
      let flag = true
      let index = 0
      //[buffer<1,2,3,4>,buffer<1,2,3,4>,buffer<1,2,3,4>]
      //每次取出快取前的第一個buffer
      while(flag && (buf = this.buffers.shift())){
        for(let i=0;i<buf.length;i++){
          //把取出的一個buffer中的資料放入新定義的buffer中
          buffer[index++] = buf[i]
          //當buffer的長度和n(引數)長度一樣時,停止迴圈
          if(index === n){
            flag = false
            //維護快取,因為可能快取中的buffer長度大於n,當取出n的長度時,還會剩下其餘的buffer,我們需要切割buf並且放到快取陣列之前
            this.len -= n
            let r = buf.slice(i+1)
            if(r.length){
              this.buffers.unshift(r)
            }
            break
          }
        }
      }
    }
    //如果快取區沒有東西,等會讀完需要觸發readable事件
    //這裡會有一種狀況,就是如果每次Readable讀取的數量正好等於highWaterMark(流讀取到快取的長度),就會每次都等於0,每次都觸發Readable事件,就會每次讀,讀到沒有為止,最後還會觸發一下null
    if(this.len === 0){
      this.emittedReadable = true
    }
    if(this.len < this.highWaterMark){
      //預設,一開始的時候開始讀取
      if(!this.reading){
        this.reading = true
        //真正多讀取操作
        this._read()
      }
    }
    return buffer&&buffer.toString()
  }
  _read(){
    if(typeof this.fd != 'number'){
      //等待著觸發open事件後fd肯定拿到了 再去執行read方法
      return this.once('open',()=>{this._read()})
    }
    //先讀這麼多buffer
    let buffer = Buffer.alloc(this.highWaterMark)
    fs.read(this.fd,buffer,0,buffer.length,this.pos,(err,byteRead)=>{
      if(byteRead > 0){
        //當第一次讀到資料後,改變reading的狀態,如果觸發read事件,可能還會在觸發第二次_read
        this.reading = false
        //每次讀到資料增加快取取得長度
        this.len += byteRead
        //每次讀取之後,會增加讀取的檔案的讀取開始位置
        this.pos += byteRead
        //將讀到的buffer放入快取區buffers中
        this.buffers.push(buffer.slice(0,byteRead))
        //觸發readable
        if(this.emittedReadable){
          this.emittedReadable = false
          //可以讀取了,預設開始的時候杯子填滿了
          this.emit('readable')
        }
      }else{
        //沒讀到就出發end事件
        this.emit('end')
      }
    })
  }
  destory(){
    if(typeof this.fd != 'number'){
      return this.emit('close')
    }
    //如果檔案被開啟過 就關閉檔案並且觸發close事件
    fs.close(this.fd,()=>{
      this.emit('close')
    })
  }
  open(){
    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{
      //有可能fd這個檔案不存在 需要做處理
      if(err){
        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){
          //銷燬(關閉檔案,觸發關閉檔案事件)
          this.destory()
        }
        //如果有錯誤 就會觸發error事件
        this.emit('error',err)
        return
      }
      //儲存檔案描述符
      this.fd = fd
      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}
複製程式碼
  • Readable和讀流的data的區別就是,Readable可以控制自己從快取區讀多少和控制讀的次數,而data是每次讀取都清空快取,讀多少輸出多少
  • 我們可以看一下下面這個例子
let rs = fs.createReadStream('./a.txt')
rs.on('data',(data)=>{
  console.log(data)
})
//因為上面的data事件把資料讀了,清空快取區。所以導致下面的readable讀出為null
rs.on('readable',()=>{
  let result = r.read(1)
  console.log(result)
})
複製程式碼

自定義可讀流

因為createReadStream內部呼叫了ReadStream類,ReadStream又實現了Readable介面,ReadStream實現了_read()方法,所以我們通過自定義一個類繼承stream模組的Readable,並在原型上自定義一個_read()就可以自定義自己的可讀流

let { Readable } = require('stream')

class MyRead extends Readable{
  //流需要一個_read方法,方法中push什麼,外面就接收什麼
  _read(){
    //push方法就是上面_read方法中的push一樣,把資料放入快取區中
    this.push('100')
    //如果push了null就表示沒有東西可讀了,停止(如果不寫,就會一直push上面的值,死迴圈)
    this.push(null)
  }
}
複製程式碼

可寫流

  • 如果檔案不存在會建立,如果有內容會被清空
  • 讀取到highWaterMark的時候就會輸出
  • 第一次是真的寫到檔案 後面就是寫入快取區 再從快取區裡面去取

引數(和可讀流的類似)

  • path:寫入的檔案的路徑
  • option:
    • highWaterMark:水位線,一次可寫入快取中的位元組,一般預設是64k
    • flags:標識,寫入檔案要做的操作,預設是w
    • encoding:編碼,預設為buffer
    • start:開始寫入的索引位置
    • end:結束寫入的索引位置(包括結束位置)
    • autoClose:寫入完畢是否關閉,預設為true
let ReadStream = require('./ReadStream')
//讀取的時候預設讀64k 
let rs = new ReadStream('./a.txt',{
  highWaterMark: 2,//一次讀的位元組 預設64k
  flags: 'r',      //標示 r為讀 w為寫
  autoClose: true, //預設讀取完畢後自動關閉
  start: 0,
  end: 5,          //流是閉合區間包start,也包end 預設是讀完
  encoding: 'utf8' //預設編碼是buffer
})
複製程式碼

方法

write

let fs = require('fs')
let ws = fs.createWriteStream('./d.txt',{
  flags: 'w',
  encoding: 'utf8',
  start: 0,
  //write的highWaterMark只是用來觸發是不是幹了
  highWaterMark: 3 //寫是預設16k
})
//返回boolean 每當write一次都會在ws中吃下一個饅頭 當吃下的饅頭數量達到highWaterMark時 就會返回false 吃不下了會把其餘放入快取 其餘狀態返回true
//write只能放string或者buffer
flag = ws.write('1','utf8',()=>{
  console.log(i)
})
複製程式碼

drain

//drain只有嘴塞滿了 吃完(包括記憶體中的,就是地下的)才會觸發 這裡是兩個條件 一個是必須是吃下highWaterMark個饅頭 並且在吃完的時候才會callback
ws.on('drain',()=>{
  console.log('幹了')
})
複製程式碼

渴望力量嗎?少年!流的原理

fs.write():可讀流底層呼叫的就是這個方法,最原生的讀方法

//wfd檔案描述符,一般通過fs.open中獲取
//buffer,要取資料的快取源
//0,從buffer的0位置開始取
//BUFFER_SIZE,每次取BUFFER_SIZE這麼長的長度
//index,每次寫入檔案的index的位置
//bytesRead,真實寫入的個數
fs.write(wfd,buffer,0,bytesRead,index,function(err,bytesWrite){

})
複製程式碼

通過程式碼實現

let fs = require('fs')
let EventEmitter = require('events')
//只有第一次write的時候直接用_write寫入檔案 其餘都是放到cache中 但是len超過了highWaterMark就會返回false告知需要drain 很佔快取
//從第一次的_write開始 回去一直通過clearBuffer遞迴_write寫入檔案 如果cache中沒有了要寫入的東西 會根據needDrain來判斷是否觸發乾點
class WriteStream extends EventEmitter{
  constructor(path,options = {}){
    super()
    this.path = path
    this.highWaterMark = options.highWaterMark || 64*1024
    this.flags = options.flags || 'r'
    this.start = options.start || 0
    this.pos = this.start
    this.autoClose = options.autoClose || true
    this.mode = options.mode || 0o666
    //預設null就是buffer
    this.encoding = options.encoding || null

    //開啟這個檔案
    this.open()

    //寫檔案的時候需要哪些引數
    //第一次寫入的時候 是給highWaterMark個饅頭 他會硬著頭皮寫到檔案中 之後才會把多餘吃不下的放到快取中
    this.writing = false
    //快取陣列
    this.cache = []
    this.callbackList = []
    //陣列長度
    this.len = 0
    //是否觸發drain事件
    this.needDrain = false
  }

  clearBuffer(){
    //取快取中最上面的一個
    let buffer = this.cache.shift()
    if(buffer){
      //有buffer的情況下
      this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer(),buffer.callback)
    }else{
      //沒有的話 先看看需不需要drain
      if(this.needDrain){
        //觸發drain 並初始化所有狀態
        this.writing = false
        this.needDrain = false
        this.callbackList.shift()()
        this.emit('drain')
        
      }
      this.callbackList.map(v=>{
        v()
      })
      this.callbackList.length = 0
    }
  }
  _write(chunk,encoding,clearBuffer,callback){
    //因為write方法是同步呼叫的 所以可能還沒獲取到fd
    if(typeof this.fd != 'number'){
      //直接在open的時間物件上註冊一個一次性事件 當open被emit的時候會被呼叫
      return this.once('open',()=>this._write(chunk,encoding,clearBuffer,callback))
    }
    fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWrite)=>{
      this.pos += byteWrite
      //每次寫完 相應減少記憶體中的數量
      this.len -= byteWrite
      if(callback) this.callbackList.push(callback)
      //第一次寫完
      clearBuffer()
      
    })
  }

  //寫入方法
  write(chunk,encoding=this.encoding,callback){
    //判斷chunk必須是字串或者buffer 為了統一都變成buffer
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding)
    //維護快取的長度 3
    this.len += chunk.length
    let ret = this.len < this.highWaterMark
    if(!ret){
      //表示要觸發drain事件
      this.needDrain = true
    }
    //正在寫入的應該放到記憶體中
    if(this.writing){
      this.cache.push({
        chunk,
        encoding,
        callback
      })
    }else{
      //這裡是第一次寫的時候
      this.writing = true
      //專門實現寫的方法
      this._write(chunk,encoding,()=>this.clearBuffer(),callback)
    }
    // console.log(ret)
    //能不能繼續寫了 false代表下次寫的時候更佔記憶體
    return ret
  }

  destory(){
    if(typeof this.fd != 'number'){
      return this.emit('close')
    }
    //如果檔案被開啟過 就關閉檔案並且觸發close事件
    fs.close(this.fd,()=>{
      this.emit('close')
    })
  }
  open(){
    //fd表示的就是當前this.path的這個檔案,從3開始(number型別)
    fs.open(this.path,this.flags,(err,fd)=>{
      //有可能fd這個檔案不存在 需要做處理
      if(err){
        //如果有自動關閉 則幫他銷燬
        if(this.autoClose){
          //銷燬(關閉檔案,出發關閉檔案事件)
          this.destory()
        }
        //如果有錯誤 就會觸發error事件
        this.emit('error',err)
        return
      }
      //儲存檔案描述符
      this.fd = fd
      //當檔案開啟成功時觸發open事件
      this.emit('open',this.fd)
    })
  }
}
複製程式碼

自定義可寫流

因為createWriteStream內部呼叫了WriteStream類,WriteStream又實現了Writable介面,WriteStream實現了_write()方法,所以我們通過自定義一個類繼承stream模組的Writable,並在原型上自定義一個_write()就可以自定義自己的可寫流

let { Writable } = require('stream')

class MyWrite extends Writable{
  _write(chunk,encoding,callback){
    //write()的第一個引數,寫入的資料
    console.log(chunk)
    //這個callback,就相當於我們上面的clearBuffer方法,如果不執行callback就不會繼續從快取中取出寫
    callback()
  }
}

let write = new MyWrite()
write.write('1','utf8',()=>{
  console.log('ok')
})
複製程式碼

pipe

管道流,是可讀流上的方法,至於為什麼放到這裡,主要是因為需要2個流的基礎知識,是可讀流配合可寫流的一種傳輸方式。如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫耗記憶體,但用了pipe就不會了,始終用規定的記憶體。

用法

let fs = require('fs')
//pipe方法叫管道 可以控制速率
let rs = fs.createReadStream('./d.txt',{
  highWaterMark: 4
})
let ws = fs.createWriteStream('./e,txt',{
  highWaterMark: 1
})
//會監聽rs的on('data')將讀取到的資料,通過ws.write的方法寫入檔案
//呼叫寫的一個方法 返回boolean型別
//如果返回false就呼叫rs的pause方法 暫停讀取
//等待可寫流 寫入完畢在監聽drain resume rs
rs.pipe(ws) //會控制速率 防止淹沒可用記憶體
複製程式碼

自己實現一下

let fs = require('fs')
//這兩個是上面自己寫的ReadStream和WriteStream
let ReadStream = require('./ReadStream')
let WriteStream = require('./WriteStream')

//如果用原來的讀寫,因為寫比較耗時,所以會多讀少寫,耗記憶體
ReadStream.prototype.pipe = function(dest){
  this.on('data',(data)=>{
    let flag = dest.write(data)
    //如果寫入的時候嘴巴吃滿了就不繼續讀了,暫停
    if(!flag){
      this.pause()
    }
  })
  //如果寫的時候嘴巴里的吃完了,就會繼續讀
  dest.on('drain',()=>{
    this.resume()
  })
  this.on('end',()=>{
    this.destory()
    //清空快取中的資料
    fs.fsync(dest.fd,()=>{
      dest.destory()
    })
  })
}
複製程式碼

雙工流

有了雙工流,我們可以在同一個物件上同時實現可讀和可寫,就好像同時繼承這兩個介面。 重要的是雙工流的可讀性和可寫性操作完全獨立於彼此。這僅僅是將兩個特性組合成一個物件。

let { Duplex } = require('stream')
//雙工流,可讀可寫
class MyDuplex extends Duplex{
  _read(){
    this.push('hello')
    this.push(null)
  }
  _write(chunk,encoding,clearBuffer){
    console.log(chunk)
    clearBuffer()
  }
}

let myDuplex = new MyDuplex()
//process.stdin是node自帶的process程式中的可讀流,會監聽命令列的輸入
//process.stdout是node自帶的process程式中的可寫流,會監聽並輸出在命令列中
//所以這裡的意思就是在命令列先輸出hello,然後我們輸入什麼他就出來對應的buffer(先作為可讀流出來)
process.stdin.pipe(myDuplex).pipe(process.stdout)
複製程式碼

轉換流

轉換流的輸出是從輸入中計算出來的。對於轉換流,我們不必實現readwrite的方法,我們只需要實現一個transform方法,將兩者結合起來。它有write方法的意思,我們也可以用它來push資料。

let { Transform } = require('stream')

class MyTransform extends Transform{
  _transform(chunk,encoding,callback){
    console.log(chunk.toString().toUpperCase())
    callback()
  }
}
let myTransform = new MyTransform()


class MyTransform2 extends Transform{
  _transform(chunk,encoding,callback){
    console.log(chunk.toString().toUpperCase())
    this.push('1')
    // this.push(null)
    callback()
  }
}
let myTransform2 = new MyTransform2()

//此時myTransform2被作為可寫流觸發_transform,輸出輸入的大寫字元後,會通過可讀流push字元到下一個轉換流中
//當寫入的時候才會觸發transform的值,此時才會push,所以後面的pipe拿到的chunk是前面的push的值
process.stdin.pipe(myTransform2).pipe(myTransform)
複製程式碼

總結

可讀流

  • 在 flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。
  • 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。
  • 所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式:
    • 監聽 'data' 事件
    • 呼叫 stream.resume() 方法
    • 呼叫 stream.pipe() 方法將資料傳送到 Writable
  • 可讀流可以通過下面途徑切換到 paused 模式:
    • 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
    • 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。

可寫流

  • 需要知道只有在真正的吃滿了,並且等到把嘴裡的和地上的饅頭(快取中的)都吃下了才會觸發drain事件
  • 第一次寫入會直接寫入檔案中,後面會從快取中一個個取

雙工流

  • 只是對可寫可讀流的一種應用,既可作為可讀流,也能作為可寫流,並且作為可讀或者可寫時時隔離

轉換流

  • 一般轉換流是邊輸入邊輸出的,而且一般只有觸發了寫入操作時才會進入_transform方法中。跟雙工流的區別就是,他的可讀可寫是在一起的。

OK,講完收工,從此你就是魔王

渴望力量嗎?少年!流的原理

相關文章