Node.js Stream(流) 簡單易懂全解析

nightZing發表於2018-02-03

一.node.js中的流是什麼

       stream(流)是Node.js提供的又一個僅在服務區端可用的模組,流是一種抽象的資料結構。Stream 是一個抽象介面,Node 中有很多物件實現了這個介面。例如,對http 伺服器發起請求的request 物件就是一個 Stream,還有stdout(標準輸出流)。
       顧名思義,流的意思就是資料的流動,就好比停水了,樓上的人存了一些水,樓下的人發請求想借樓上的人一桶水,直接搬費力又麻煩,直接往下倒容易倒到地上,於是可以用一根管子連線兩個桶(A和B),樓上的人直接把A桶裡的水通過管子流到樓下的B桶裡,這就類似於request物件向伺服器發請求要資源,在這request 請求資源的傳播方式通過流來實現。
       再舉個例子,可以把資料看成是資料流,如果我們在鍵盤上打字,電腦程式把字元一個一個輸出到螢幕上,這也可以看成是一個流,這個可以叫做標準輸出(stdout)。

二.為什麼要在node中使用流

看了前面稍微瞭解node的同學可能就要問了,流的作用不就是傳遞資料麼,也就是把一個地方資料拷貝到另一個地方,不用流也可以這樣實現:

var water = fs.readFileSync('a.txt', {encoding: 'utf8'});
fs.writeFileSync('b.txt', water);
複製程式碼

是的,只要使用node的讀寫檔案的功能就能實現上面借水的效果,但這樣做有個致命問題:

  • 處理資料量較大的檔案時不能分塊處理,導致速度慢,記憶體容易爆滿。

使用讀寫方式是把檔案內容全部讀入記憶體,然後再寫入檔案,對於小型的文字檔案問題不大,但是遇到較大的比如音訊、視訊檔案,動輒幾個GB大小實在承受不住,而流可以把檔案資源拆分成小塊,一塊一塊的運輸,資源就像水流一樣進行傳輸,使用流的話上述功能可以這樣寫:

var fs = require('fs');
var readStream = fs.createReadStream('a.mp4'); // 建立可讀流
var writeStream = fs.createWriteStream('b.mp4'); // 建立可寫流

readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料
    writeStream.write(chunk);
});

readStream.on('end', function() { // 當沒有資料時,關閉資料流
    writeStream.end();
});
複製程式碼
  • 但這樣寫還是有一些問題的,如果說寫入的速度跟不上讀取的速度,有可能導致資料丟失。正常的情況應該是,寫完一段,再讀取下一段,如果沒有寫完的話,就讓讀取流先暫停,等寫完再繼續,所以為了讓可讀流和可寫流速度一致,就要用到流中必不可少的屬性pipe了,pipe翻譯過來意思是管道,顧名思義,就想上面的倒水一樣,如果不用一根管子相連,A桶倒進B桶的水不會均速傳輸,可能會導致水的浪費,用pipe可以這樣解決上述問題:
fs.createReadStream('a.mp4').pipe(fs.createWriteStream('b.mp4));
// pipe自動呼叫了data,end等事件
複製程式碼
  • 需要特別注意的是,pipe()只是可讀流的方法,也就是說只能從可讀流中通過pipe方法拷貝資料到可寫流,反之則不行,寫的時候要注意順序。

三.流的四種型別

Stream提供了以下四種型別的流:

  1. Readable 可讀流
  2. Writable 可寫流
  3. Duplex 可讀可寫流
  4. Transform 在讀寫過程中可以修改和變換資料的Duplex流

1.Readable
可讀流有五個引數:

  • highWaterMark 快取區位元組大小,預設16384
  • encoding 字元編碼,預設為null,就是buffer
  • objectMode 是否操作js其他型別 預設false
  • read 對內部的_read()方式實現 子類實現,父類呼叫
  • destroy 對內部的_ destroy()方法實現 子類實現,父類呼叫

可讀流中分為2種模式流動模式和暫停模式。

1、流動模式:可讀流自動讀取資料,通過EventEmitter介面的事件儘快將資料提供給應用。
2、暫停模式:必須顯式呼叫stream.read()方法來從流中讀取資料片段。

暫停模式切換到流動模式i:

1、監聽“data”事件
2、呼叫 stream.resume()方法
3、呼叫 stream.pipe()方法將資料傳送到可寫流

流動模式切換到暫停模式:

1、如果不存在管道目標,呼叫stream.pause()方法
2、如果存在管道目標,呼叫 stream.unpipe()並取消'data'事件監聽
可讀流事件:'data','readable','error','close','end'

監聽data事件,觸發流動模式

const { Readable } = require('stream');
   
    let i = 0;
       
    const rs = Readable({
        encoding: 'utf8',
        // 這裡傳入的read方法,會被寫入_read()
        read: (size) => {
            // size 為highWaterMark大小
            // 在這個方法裡面實現獲取資料,讀取到資料呼叫rs.push([data]),如果沒有資料了,push(null)結束流
            if (i < 6) {
                rs.push(`當前讀取資料: ${i++}`);
            } else {
                rs.push(null);
            }
        },
        // 原始碼,可覆蓋
        destroy(err, cb) {
            rs.push(null);
            cb(err);
        }
    });
        
    rs.on('data', (data) => {
        console.log(data);
        // 每次push資料則觸發data事件
複製程式碼

監聽readable事件,觸發暫停模式,當流有了新資料或到了流結束之前觸發readable事件,需要顯示呼叫read([size])讀取資料:

    const { Readable } = require('stream');
        
    let i = 0;
        
    const rs = Readable({
        encoding: 'utf8',
        highWaterMark: 9,
        // 這裡傳入的read方法,會被寫入_read()
        read: (size) => {
            // size 為highWaterMark大小
            // 在這個方法裡面實現獲取資料,讀取到資料呼叫rs.push([data]),如果沒有資料了,push(null)結束流
            if (i < 10) {
              // push其實是把資料放入快取區
              rs.push(`當前讀取資料: ${i++}`);
            } else {
                rs.push(null);
            }
        }
    });
    
    rs.on('readable', () => {
        const data = rs.read(9);
        console.log(data);
        // 
    })
複製程式碼

2. Writable
可寫流有以下引數:

highWaterMark 快取區位元組大小,預設16384 decodeStrings 是否將字元編碼傳入緩衝區 objectMode 是否操作js其他型別 預設false write 子類實現,供父類呼叫 實現寫入底層資料 writev 子類實現,供父類呼叫 一次處理多個chunk寫入底層資料 destroy 可以覆蓋父類方法,不能直接呼叫,銷燬流時,父類呼叫 final 完成寫入所有資料時父類觸發

const Writable = require('stream').Writable

const writable = Writable()
// 實現`_write`方法
// 這是將資料寫入底層的邏輯
writable._write = function (data, enc, next) {
  // 將流中的資料寫入底層
  process.stdout.write(data.toString().toUpperCase())
  // 寫入完成時,呼叫`next()`方法通知流傳入下一個資料
  process.nextTick(next)
}

// 所有資料均已寫入底層
writable.on('finish', () => process.stdout.write('DONE'))

// 將一個資料寫入流中
writable.write('a' + '\n')
writable.write('b' + '\n')
writable.write('c' + '\n')

// 再無資料寫入流時,需要呼叫`end`方法
writable.end()
複製程式碼

3. Duplex
Duplex為讀寫流,既可當成可讀流來使用,也可當成可寫流來使用,實際上就是繼承了Readable和Writable的一類流。所以,Duplex擁有Writable和Readable所有方法和事件,但各自獨立快取區,一個Duplex物件可以同時實現_read()和_write()方法。

var Duplex = require('stream').Duplex

var duplex = Duplex()

// 可讀端底層讀取邏輯
duplex._read = function () {
  this._readNum = this._readNum || 0
  if (this._readNum > 1) {
    this.push(null)
  } else {
    this.push('' + (this._readNum++))
  }
}

// 可寫端底層寫邏輯
duplex._write = function (buf, enc, next) {
  // a, b
  process.stdout.write('_write ' + buf.toString() + '\n')
  next()
}

// 0, 1
duplex.on('data', data => console.log('ondata', data.toString()))

duplex.write('a')
duplex.write('b')

duplex.end()
複製程式碼

4. Transform
Tranform為轉換流,它繼承自Duplex,並已經實現了_read和_write方法,同時要求使用者實現一個_transform方法,從一個地方讀取資料,轉換資料後輸出到一個地方。

    const stream = require('stream');
    
    const transform = stream.Transform({
        transform(chunk, encoding, cb){
            // 把資料轉換成小寫字母,然後push到快取區
            this.push(chunk.toString().toLowerCase());
            cb();
        }
    });
    
    transform.write('D');
    
    console.log(transform.read(1).toString()); // d
複製程式碼

四.stream中的pipe

前面已經說過,pipe的作用是在流中搭建一條管道,從可讀流中到可寫流,目的是實現讀取和寫入步調一致,邊讀邊寫。

   const stream = require('stream');
    
    const readStream = stream.Readable({
        read() {
            this.push(fs.readFileSync('a.txt')); // 檔案內容 test
            this.push(null);
        }
    });
    
    const writeStream = stream.Writable({
        write(chunk, encoding, cb) {
            // chunk為test buffer
            fs.writeFileSync('b.txt', chunk.toString());
            cb();
        }
    });
    
    writeStream.on('pipe', data => {
        // 觸發pipe事件
        console.log(data);
    });
    
    readStream.pipe(writeStream);
複製程式碼

相關文章