面試高階前端工程師必問之流-stream

xlei1123發表於2018-07-09

流(stream)是一種在 Node.js 中處理流式資料的抽象介面。 stream 模組提供了一些基礎的 API,用於構建實現了流介面的物件。

Node.js 提供了多種流物件。 例如,傳送到 HTTP 伺服器的請求和 process.stdout 都是流的例項。

流可以是可讀的、可寫的、或是可讀寫的。 所有的流都是 EventEmitter 的例項。

流的型別

Node.js 中有四種基本的流型別(本篇主要說前兩種):

  • Writable - 可寫入資料的流(例如 fs.createWriteStream())
  • Readable - 可讀取資料的流(例如 fs.createReadStream())
  • Duplex - 可讀又可寫的流(例如 net.Socket)
  • Transform - 在讀寫過程中可以修改或轉換資料的 Duplex 流(例如 zlib.createDeflate())

緩衝

流中一個相當重要的概念,無論讀寫流都是通過緩衝來實現的。 可寫流和可讀流都會在一個內部的緩衝器中儲存資料,可以分別使用的 writable.writableBuffer 或 readable.readableBuffer 來獲取,可緩衝的資料的數量取決於傳入流建構函式的 highWaterMark 選項,預設情況下highWaterMark 64*1024個位元組 讀寫的過程都是將資料讀取寫入緩衝,然後在將資料讀出或者寫入檔案。

幾個重要的底層方法

  1. writable.write(chunk[, encoding][, callback]) writable.write() 方法向流中寫入資料,並在資料處理完成後呼叫 callback 。如果有錯誤發生, callback 不一定 以這個錯誤作為第一個引數並被呼叫。要確保可靠地檢測到寫入錯誤,應該監聽 'error' 事件。 在確認了 chunk 後,如果內部緩衝區的大小小於建立流時設定的 highWaterMark 閾值,函式將返回 true 。 如果返回值為 false ,應該停止向流中寫入資料,直到 'drain' 事件被觸發。 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 'drain' 事件就會被觸發。
  2. readable.read([size])

來一個小例子,有助於理解

// pipe
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
    highWaterMark:1
})
let ws = fs.createWriteStream('./5.txt',{
    highWaterMark:2
})
let index = 1;
rs.on('data', (data) => {
    console.log(index++)
    let flag = ws.write(data);    // 當內部的可寫緩衝的總大小小於 highWaterMark 設定的閾值時,
    //呼叫 writable.write() 會返回 true。 一旦內部緩衝的大小達到或超過 highWaterMark 時,則會返回 falseif (!flag) {     //內部緩衝超過highWaterMark
        rs.pause()
    }
})
let wsIndex = 1;
ws.on('drain', () => {
    console.log('ws'+wsIndex++)
    rs.resume()
})
// 1 2 ws1 3 4 ws2 5 6 ws3
複製程式碼

幾個重要的事件監聽

前面已經說了所有的流都是 EventEmitter 的例項,那麼就可以on,可以emit等等

  1. rs.on('data',()) //讀入緩衝
  2. ws.on('drain',()) //寫的緩衝被清空
    上面的例子中 當寫緩衝大於highWaterMark時 我們就要暫停讀取,等待監聽到drain事件,然後重新啟動rs.resume()讀取

其實啊,在工作中也是很少直接這用到的,我們可以直接用pipe rs.pipe(ws)即可 這樣就給一個可讀流寫入到一個可寫流當中

自己實現的可讀流

let EventEmitter = require('events');   //所有的流都是 EventEmitter 的例項,流繼承EventEmitter
let fs = require('fs');
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.autoClose = options.autoClose || true;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.start = options.start || 0;
    this.end = options.end || null;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    // 應該有一個讀取檔案的位置 可變的(可變的位置)
    this.pos = this.start;
    // 控制當前是否是流動模式
    this.flowing = null;
    // 構建讀取到的內容的buffer
    this.buffer = Buffer.alloc(this.highWaterMark);
    // 當建立可讀流 要將檔案開啟
    this.open(); // 非同步執行
    this.on('newListener', (type) => {
      if(type === 'data'){ // 使用者監聽了data事件,就開始讀取吧
        this.flowing = true;
        this.read();// 開始讀取檔案
      }
    });
  }
  read(){
    // 這時候檔案還沒有開啟呢,等待著檔案開啟後再去讀取
    if(typeof this.fd !== 'number'){
      // 等待著檔案開啟,再次呼叫read方法
      return this.once('open',()=>this.read());
    }
    // 開始讀取了
    // 檔案可能有10個字串
    // start 0 end 4
    // 每次讀三個 3
    // 0-2
    // 34
    let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark
    // 檔案描述符 讀到哪個buffer裡 讀取到buffer的哪個位置
    // 往buffer裡讀取幾個,讀取的位置
    fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
      if (bytesRead>0){ // 讀到內容了
        this.pos += bytesRead;
        // 保留有用的
        let r = this.buffer.slice(0, bytesRead);
        r = this.encoding ? r.toString(this.encoding) : r;
        // 第一次讀取
        this.emit('data', r);
        if (this.flowing) {
          this.read();
        }
      }else{
        this.end = true;
        this.emit('end');
        this.destroy();
      }
    });
  }
  destroy() { // 判斷檔案是否開啟 (將檔案關閉掉)
    if (typeof this.fd === 'number') {
      fs.close(this.fd, () => {
        this.emit('close');
      });
      return;
    }
    this.emit('close');
  }
  open() { // 開啟檔案的邏輯
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err);
        if (this.autoClose) {
          this.destroy(); // 銷燬 關閉檔案(觸發close事件)
        } return;
      }
      this.fd = fd;
      this.emit('open'); // 觸發檔案開啟事件
    });
  }
  pause(){
    this.flowing = false;
  }
  resume(){
    this.flowing = true;
    this.read(); // 繼續讀取
  }
}
module.exports = ReadStream;
複製程式碼

自己實現的可寫流

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.mode = options.mode || 0o666;
    this.highWaterMark = options.highWaterMark || 16*1024;
    this.start = options.start || 0;
    this.autoClose = options.autoClose|| true;
    this.encoding = options.encoding || 'utf8';

    // 是否需要觸發drain事件
    this.needDrain = false;
    // 是否正在寫入
    this.writing = false;
    // 快取 正在寫入就放到快取中
    this.buffer = [];
    // 算一個當前快取的個數
    this.len = 0;
    // 寫入的時候也有位置關係
    this.pos = this.start;
    this.open();
  }
  // 0 [1 2] 
  write(chunk, encoding = this.encoding,callback){
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
    this.len += chunk.length;// 每次呼叫write就統計一下長度
    this.needDrain = this.highWaterMark <= this.len; 
    // this.fd
    if(this.writing){
      this.buffer.push({chunk,encoding,callback});
    }else{
      // 當檔案寫入後 清空快取區的內容
      this.writing = true;  // 走快取
      this._write(chunk,encoding,()=>this.clearBuffer());
    }
    return !this.needDrain; // write 的返回值必須是true / false   
    
    //這時候可以回頭看一下上面的例子,在this.len >= this.higWaterMark的時候,返回了一個fasle,例子中就暫停讀取了。等待寫入完成
  }
  _write(chunk,encoding,callback){
    if (typeof this.fd !== 'number') {
      return this.once('open', () => this._write(chunk, encoding, callback));
    }
    // fd是檔案描述符 chunk是資料 0 寫入的位置和 長度 , this.pos偏移量
    fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{
      this.pos += bytesWritten;
      this.len -= bytesWritten; // 寫入的長度會減少
      callback();
    });
  }
  clearBuffer(){
    let buf = this.buffer.shift();
    if(buf){
      this._write(buf.chunk, buf.encoding, () => this.clearBuffer());
    }else{
      this.writing = false;
      this.needDrain = false; // 觸發一次drain  再置回false 方便下次繼續判斷
      this.emit('drain');
    }
  }
  destroy(){
    if(typeof this.fd === 'number'){
      fs.close(this.fd,()=>{
        this.emit('close');
      });
      return 
    }
    this.emit('close');
  }
  open(){
    fs.open(this.path,this.flags,this.mode,(err,fd)=>{
      if(err){
        this.emit('error');
        this.destroy();
        return 
      }
      this.fd = fd;
      this.emit('open');
    });
  }
}
module.exports = WriteStream;
複製程式碼

以上就是流的一些基礎知識,流的簡單應用以及自己實現的可讀流可寫流。當然有很多不足之處,希望朋友們提出指正。也希望和各位朋友一起學習分享!

相關文章