全棧前端工程師必會的流,瞭解下。。。

嗯_嗯發表於2018-07-14

首先啥是流呢?

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

Node.js 中有四種基本的流型別

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

流中的資料有兩種模式,二進位制模式和物件模式

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

所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。

下面就簡單的介紹下 首先是可讀流

可讀流事實上工作在下面兩種模式之一:flowing 和 paused

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

如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 'data' 事件,或是取消了 'data' 事件監聽,就有可能出現這種情況。

可讀流的三種狀態

在任意時刻,任意可讀流應確切處於下面三種狀態之一:

  • readable._readableState.flowing = null

  • readable._readableState.flowing = false

  • readable._readableState.flowing = true

  • 若 readable._readableState.flowing 為 null,由於不存在資料消費者,可讀流將不會產生資料。 在這個狀態下,監聽 'data' 事件,呼叫 readable.pipe() 方法,或者呼叫 readable.resume() 方法, readable._readableState.flowing 的值將會變為 true 。這時,隨著資料生成,可讀流開始頻繁觸發事件。

  • 呼叫 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背壓”(back pressure), 將導致 readable._readableState.flowing 值變為 false。 這將暫停事件流,但 不會 暫停資料生成。 在這種情況下,為 'data' 事件設定監聽函式不會導致 readable._readableState.flowing 變為 true。

  • 當 readable._readableState.flowing 值為 false 時, 資料可能堆積到流的內部快取中。

建立可讀流


let rs=fs.createReadStream(path.join(__dirname,'1.txt'),{
    flags:'r',//檔案的操作 預設r
    encoding:'utf8',//編碼 預設buffer
    autoClose:true,//是否自動關閉  預設true
    start:0,//開始讀取的位置 預設0
    end:10,//結束讀取的位置(包含)預設讀完
    highWaterMark:3,// 一次讀取多少位元組 預設讀是64k 也就是64*1024
    //還有fd 檔案描述符 預設null ,mode 檔案操作許可權,預設為 0o666
})
//這裡的第一個引數路徑;要說下: 是相對於專案根目錄的;跟我們平常的相對路徑是不一樣的;
//返回值是一個讀流物件;

// 監聽data事件;獲取每一次讀到的內容;流切換到流動模式,資料會被儘可能快的讀出
rs.on('data',function (data) {
    console.log(data);
    
});
//監聽end事件 該事件會在讀完資料後被觸發
rs.on('end', function () {
    console.log('讀取完成');
});
//還有open close error一些事件 看名字就知道幹啥的
//暫停和恢復觸發data;通過pause()方法和resume()方法
rs.on('data', function (data) {
    rs.pause();
    console.log(data);
});
setTimeout(function () {
    rs.resume();
},2000);

複製程式碼

可寫流

//和讀流引數基本差不多 
let ws=fs.createWriteStream('1.txt', {
    flags: 'w', //預設是'w'
    encoding: 'utf8',//預設是utf8
    highWaterMark: 2, // 寫入快取區的預設大小 預設是16k 16*1024
})
let flag=ws.write('aaaa','utf8',()=>{
    console.log('寫入成功');
    
})
//引數:1.寫入的資料buffer/string  2.編碼編碼格式chunk為字串時有用可選, 3.寫入成功後的回撥
//返回值為布林值,系統快取區滿時為false,未滿時為true
//end方法;不是on('end',cb);在調完end方法後;不能再呼叫write方法
ws.end(chunk,[encoding],[callback]);
//表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 
ws.on('finish', () => {
  console.error('所有的寫入已經完成!');
});
//在呼叫了 ws.end() 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發。

//drain方法  
//當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 'drain' 事件就會被觸發
//建議, 一旦 write() 返回 false, 在 'drain' 事件觸發前, 不能寫入任何資料塊

//感受下
let i = 10;

function write() {
    let flag = true;
    while (i && flag) {
        flag = ws.write("1");
        i--;
        console.log(flag);
    }
}
write(); //true false
ws.on('drain', () => { 
    console.log("drain");
    write();
});

//true false  drain  5次



複製程式碼

pipe 用法

var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
複製程式碼

原理

var ws = fs.createWriteStream('./2.txt');
var rs = fs.createReadStream('./1.txt');
rs.on('data', function (data) {
    var flag = ws.write(data);
    if (!flag)
        rs.pause();
});
ws.on('drain', function () {
    rs.resume();
});
rs.on('end', function () {
    ws.end();
});
//將資料的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用記憶體。
複製程式碼

簡單的實現下可讀流

let fs = require('fs');
let EventEmitter = require('events');

class ReadStream extends EventEmitter {//可以on就是釋出訂閱 
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.fd = options.fd;
        this.flags = options.flags || 'r';
        this.encoding = options.encoding;
        this.start = options.start || 0;
        this.pos = this.start;
        this.end = options.end;
        this.flowing = false;
        this.autoClose = true;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.length = 0;
        this.on('newListener', (type, listener) => {
            if (type == 'data') {//檢測到有監聽data的事件  就變成流動模式
                this.flowing = true;
                this.read();
            }
        });
        this.on('end', () => {
            if (this.autoClose) {
                this.destroy();
            }
        });
        this.open();//只要呼叫這個類 預設開啟檔案
    }

    read() {
        if (typeof this.fd != 'number') {
            return this.once('open', () => this.read());
        }
        let n = this.end ? Math.min(this.end - this.pos, this.highWaterMark) : this.highWaterMark;
        
        fs.read(this.fd,this.buffer,0,n,this.pos,(err,bytesRead)=>{
        //引數: 檔案描述符  讀到哪裡(容器) 從哪開始 讀到哪個位置 偏移 回撥 
            if(err){
             return;
            }
            if(bytesRead){
                let data = this.buffer.slice(0,bytesRead);
                data = this.encoding?data.toString(this.encoding):data;
                this.emit('data',data);
                this.pos += bytesRead;
                if(this.end && this.pos > this.end){
                  return this.emit('end');
                }
                if(this.flowing)
                    this.read();
            }else{
                this.emit('end');
            }
        })
    }

    open() {
    //開啟檔案 (非同步)
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) return this.emit('error', err);
            this.fd = fd;
            this.emit('open', fd);
        })
    }


    end() {
        if (this.autoClose) {
            this.destroy();
        }
    }

    destroy() {
    //關閉檔案
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }

}

module.exports = ReadStream;
複製程式碼

簡單的實現下可寫流

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

    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) return this.emit('error', err);
            this.fd = fd;
            this.emit('open', fd);
        })
    }

    write(chunk, encoding, cb) {
        if (typeof encoding == 'function') {
            cb = encoding;
            encoding = null;
        }

        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding || 'utf8');
        let len = chunk.length;
        this.length += len;
        let ret = this.length < this.highWaterMark;
        if (this.writing) {
            this.buffers.push({
                chunk,
                encoding,
                cb,
            });
        } else {
            this.writing = true;
            this._write(chunk, encoding,this.clearBuffer.bind(this));
        }
        return ret;
    }

    _write(chunk, encoding, cb) {
        if (typeof this.fd != 'number') {
            return this.once('open', () => this._write(chunk, encoding, cb));
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, written) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                }
                return this.emit('error', err);
            }
            this.length -= written;
            this.pos += written;
            cb && cb();
        });
    }

    clearBuffer() {
        let data = this.buffers.shift();
        if (data) {
            this._write(data.chunk, data.encoding, this.clearBuffer.bind(this))
        } else {
            this.writing = false;
            this.emit('drain');
        }
    }

    end() {
        if (this.autoClose) {
            this.emit('end');
            this.destroy();
        }
    }

    destroy() {
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }

}

module.exports = WriteStream;
複製程式碼

相關文章