node基礎(4)---Stream的用法詳解

好想吃涼皮發表於2018-04-01

流是基於事件的API,用於管理和處理資料,而且有不錯的效率. 藉助事件和非阻塞I/O庫,流模組允許在其可用的時候動態處理,在其不需要的時候釋放掉.

流的概念

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

那麼說了這些,到底有哪些我們常用的流呢?下面我就一一介紹:

createReadStream

我們首先看的是可讀流,它的實現機制是在預設情況下不會將內容輸出,而是在內部先建立一個buffer,先讀取highWaterMark長度。可讀流有5個事件,分別是`data`,`close`,`open`,`end`,`error`,可讀流會瘋狂的觸發data事件,直到讀完為止,除此之外,createReadStream還有兩個事件,暫停(pause)和繼續(resume),讓我們來看一下用法:
複製程式碼
let fs = require('fs');
let path = require('path');

let re = fs.createReadStream(path.join(__dirname,'1.txt'),{
    flags:'r', // 檔案的操作是讀取操作
    encoding:'utf8', // 預設是null null代表的是buffer
    autoClose:true, // 讀取完畢後自動關閉
    highWaterMark:3,// 預設是64k  64*1024b    start:0,//start和end是包前又包後,共10個位元組
    end:9
})
re.on('open',function(){
    console.log('檔案開啟了')

})
re.on('close',function(){
    console.log('關閉')
})
//這裡採用釋出訂閱模式,內部有一個newLisenter函式來監聽例項上綁了哪些事件。而內部的事件觸發採用的是emit()
re.on('error',function(err){
    console.log(err)
})
re.on('data',function(data){//觸發data就會一直流
    console.log(data)
    re.pause()//暫停
})
setInterval(function(){
    re.resume()//繼續
},3000)
re.on('end',function(){
    console.log('end')
})
複製程式碼
通過這幾個方法一實驗就知道內部是怎麼實現的了,我這裡用vscode跑了一下:
複製程式碼

這裡寫圖片描述


createWriteStream

可寫流相對來說用法就比較簡單了,只有write,end,兩個方法。需要注意的是寫入的資料必須是字串或者buffer。我們來看一下它的用法:

let fs = require('fs');

let ws = fs.createWriteStream('./1.txt',{
    flags:'w',
    mode:0o666,
    autoClose:true,
    highWaterMark:3, // 預設寫是16k
    encoding:'utf8',
    start:0
});
// 寫入的資料必須是字串或者buffer
// flag代表是否能繼續寫
// 表示符表示的並不是是否寫入 表示的是能否繼續寫,但是返回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);

//ws.end('ok'); // 當寫完後 就不能再繼續寫了
//ws.write('123'); //write after end
複製程式碼

ws還有一個方法就是drain,抽乾方法 當都寫入完後會觸發drain事件, 必須快取區滿了 ,滿了後被清空了才會出發drain

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);
ws.on('drain',function(){
    console.log('drain')
});
複製程式碼

這個drain以後可以讓可寫流配合可讀流寫一個pipe方法。 pipe是什麼呢? 其實就是一個讀一點寫一點的方法,實現的是下面的這樣的事:

let fs = require('fs');
let path = require('path');

let rs = fs.createReadStream(path.join(__dirname,'./1.txt'),{
    highWaterMark:3
})
let ws = fs.createWriteStream(path.join(__dirname,'./2.txt'),{
    highWaterMark:1
})

rs.on('data',function(chunk){
let flags = ws.write(chunk);
if(!flags){
    rs.pause()
}
})
ws.on('drain',function(){
    console.log('幹了')
    rs.resume()
})
複製程式碼

而pipe 的實現方法就簡單的讓人感動:

rs.pipe(ws); 
複製程式碼

readable

readable也是一個很實用的可讀流的方法,它可以很智慧的的實現暫停流。 readable的實現原理是:

當我只要建立一個流 就會先把快取區 填滿,等待著你自己消費 如果當前快取區被清空後會再次觸發readable事件 當你消費小於 最高水位線時 會自動新增highWater這麼多資料

let fs = require('fs');
let path = require('path');
let rs = fs.createReadStream(path.join(__dirname,'1.txt'),{
    highWaterMark:3
});
// rs.on('data'); 正常情況下我們會直接用data,但是這個方法會一直讀
rs.on('readable',function(){
    let result = rs.read(1);
    console.log(result); //當我們讀一個時,這個時候輸出的就是一個
     result = rs.read(1);
    console.log(result);//當我們讀兩個時,這個時候輸出的就是倆個
      result = rs.read(1);
    console.log(result);//當我們讀到總共快取區的個數時,這個時候就讀完就接著觸發readable事件,
});
複製程式碼

執行結果是

這裡寫圖片描述

這個readable還有一個特別智慧的操作,當讀一個或沒讀到highWaterMark的值時,並且半天沒操作,程式會自動給你‘續杯‘。

let fs = require('fs');
let path = require('path');
let rs = fs.createReadStream(path.join(__dirname,'1.txt'),{
    highWaterMark:3
});

rs.on('readable',function(){
    let result = rs.read(1);
    console.log(rs._readableState.length); // 快取區的個數
    setTimeout(function(){
        console.log(rs._readableState.length);
    },1000)
});
複製程式碼

結果如下

這裡寫圖片描述

我想讀5個 快取區只有3個 他會更改快取區的大小再去讀取


Duplex

雙工流,讀的時候要以this.push(null)結尾,寫的時候要以 callback();結尾。

let {Duplex} =  require('stream');
// 雙工流 又能讀 又能寫,而且讀取可以沒關係(互不干擾);
let d = Duplex({
    read(){
        this.push('hello');
        this.push(null)
    },
    write(chunk,encoding,callback){
        console.log(chunk);
        callback();
    }
});
d.on('data',function(data){
    console.log(data);
});
d.write('hello');
複製程式碼

Transform

轉換流,就是Duplex 他不需要實現read write。

let {Transform} =  require('stream');

// 他的引數和可寫流一樣
let tranform1 = Transform({
    transform(chunk,encoding,callback){
        this.push(chunk.toString().toUpperCase()); // 將輸入的內容放入到可讀流中
        callback();
    }
});
let tranform2 = Transform({
    transform(chunk,encoding,callback){
        console.log(chunk.toString());
        callback();
    }
});
// 等待你的輸入
// rs.pipe(ws);
// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(tranform1).pipe(tranform2);
複製程式碼

這個寫法是為了讓輸入轉化成大寫輸出,process.stdin(標準輸入)可視為可讀流。 最後用node 執行一下這個檔案,輸入小寫字母就會轉化成大寫字母。

可讀流裡只能放buffer或者字串 物件流裡可以放物件

自定義一個流

let {Readable} = require('stream');

// 想實現什麼流 就繼承這個流
// Readable裡面有一個read()方法,預設掉_read()
// Readable中提供了一個push方法你呼叫push方法就會觸發data事件
let index = 9;
class MyRead extends Readable{
    _read(){
        // 可讀流什麼時候停止呢? 當push null的時候停止
        if(index-->0)return this.push('123');
        this.push(null);
    }
}
let mr = new MyRead;
mr.on('data',function(data){
    console.log(data);
});
複製程式碼

相關文章