流的概念
- 流是一組有序的、有起點和終點的位元組資料傳輸手段
- 流不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
- 流是一個抽象介面,被 Node 中的很多物件所實現。比如 HTTP 伺服器 request 和 response 物件都是流
- 流 是 Node.js 的核心模組,基本上都是 stream的例項,比如 process.stdout、http.clientRequest
流的好處
- 流是基於事件的 API,用於管理和處理資料,而且有不錯的效率
- 藉助事件和非阻塞 I/O 庫,流模組允許在其可用的時候動態處理,在其不需要的時候釋放掉
流中的資料有兩種模式,二進位制模式和物件模式
- 二進位制模式, 每個分塊都是 buffer 或者 string 物件
- 物件模式, 流內部處理的是一系列普通物件
所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。
Node.js 中有四種基本的流型別
- Readable-可讀流 (例如 fs.createReadStream() )
- Writable-可寫的流(例如 fs.createWriteStreame() )
- Duplex-可讀寫的流(例如 net.Socket )
- Transform-在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate() )
第一種型別:可讀流 createReadStream
建立一個可讀流
// 引入 fs(讀取檔案) 模組
let fs = require('fs');
// 建立一個可讀流
let rs = fs.createReadStream('./1.txt',{
flags:'r',
encoding:'utf8',
start:0,
autoClose:true,
end: 3,
highWaterMark:3
});
複製程式碼
API:createReadStream(path, [options]);
- path 是讀取檔案的路徑
- options 裡面有
- flags:開啟檔案要做的操作,預設為 'r'
- encoding:預設是null,null 代表的是 buffer
- start:開始讀取的索引位置
- autoClose:讀取完畢後自動關閉
- end:結束讀取的索引位置(包括結束位置)
- highWaterMark:讀取快取區預設的預設的大小 64kb (64*1024b)
如果指定 encoding 為 utf8 編碼, highWaterMark 要大於 3 個位元組
可讀流的一些監聽事件
- data 事件
- end 事件
- error 事件
- open 事件
- close 事件
各個寫法如下:
// 流切換到流動模式,資料會被儘可能快的讀出
rs.on('data',function(data){ // 暫停模式 -> 流動模式
console.log(data);
});
// 該事件會在讀完資料後被觸發
rs.on('end', function () {
console.log('讀取完成');
});
// 讀檔案失敗後被觸發
rs.on('error', function (err) {
console.log(err);
});
// 檔案開啟後被觸發
rs.on('open', function () {
console.log('檔案開啟了');
});
// 檔案關閉後被觸發
rs.on('close', function () {
console.log('關閉');
});
複製程式碼
設定編碼
與指定 {encoding:'utf8'} 效果相同,設定編碼
rs.setEncoding('utf8');
複製程式碼
暫停和恢復觸發 data
通過 pause() 方法和 resume() 方法
rs.on('data', function (data) {
console.log(data);
rs.pause(); // 暫停方法 表示暫停讀取,暫停data事件觸發
});
setTimeout(function () {
rs.resume(); // 恢復方法
},2000);
複製程式碼
第二種型別:可寫流 createWriteStream
建立一個可寫流
// 引入 fs(讀取檔案) 模組
let fs = require('fs');
// 建立一個可寫流
let ws = fs.createWriteStream('./1.txt',{
flags:'w',
encoding:'utf8',
highWaterMark:3
});
複製程式碼
API:createWriteStream(path, [options]);
- path 是讀取檔案的路徑
- options 裡面有
- flags:開啟檔案要做的操作,預設為 'w'
- encoding:預設是 utf8
- highWaterMark:寫入快取區的,預設大小 16kb
可寫流的一些方法
1. write 方法
ws.write(chunk, [encoding], [callback]);
複製程式碼
- chunk 寫入的資料 buffer/string
- encoding 編碼格式,chunk 為字串時有用,是個可選引數
- callback 寫入成功後的回撥
返回值為布林值,系統快取區滿時為 false,未滿時為 true
2. end 方法
ws.end(chunk, [encoding], [callback]);
複製程式碼
表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 'finish' 事件的回撥函式
3. drain 方法
ws.on('drain',function(){
console.log('drain')
});
複製程式碼
- 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false
- 當前所有快取的資料塊滿了,滿了之後情況才會出發 drain
- 一旦 write() 返回 false, 在 'drain' 事件觸發前, 不能寫入任何資料塊
4. finish 方法
ws.end('結束');
ws.on('finish',function(){
console.log('drain')
});
複製程式碼
- 在呼叫 end 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發
第三種型別:可讀寫的流,也叫雙工流(Duplex)
雙工流,可以在同一個物件上同時實現可讀、可寫,就好像同時繼承這兩個介面。而且讀取可以沒關係(互不干擾)
// 引入雙工流模組
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)
- 轉換流輸出是從輸入中計算出來的
- 轉換流中,不需要實現 read 和 write 方法,只需要實現一個 transform 方法,就可以結合兩者。
// 引入轉換流
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();
}
});
process.stdin.pipe(tranform1).pipe(tranform2);
複製程式碼
pipe 方法
大家都知道,想把 Readable 的資料 寫到 Writable,需要手動將資料讀入記憶體中,然後在寫入 Writable。也就是每次傳遞資料的時候,都需要寫一下的程式碼:
readable.on('readable', (err) => {
if(err) throw err
writable.write(readable.read())
})
複製程式碼
為了方便使用,Node.js 提供了 pipe() 方法
readable.pipe(writable)
複製程式碼
pipe 方法的原理
var fs = require('fs');
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();
});
複製程式碼
unpipe 用法
- readable.unpipe() 方法將之前通過 stream.pipe() 方法繫結的流分離
- 如果 destination 沒有傳入, 則所有繫結的流都會被分離
let fs = require('fs');
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
setTimeout(() => {
console.log('關閉向2.txt的寫入');
from.unpipe(writable);
console.log('手工關閉檔案流');
to.end();
}, 1000);
複製程式碼
cork & uncork
- 呼叫 writable.cork() 方法將強制所有寫入資料都存到記憶體中的緩衝區裡。 直到呼叫 stream.uncork() 或 stream.end() 方法時,緩衝區裡的資料才會被輸出
- writable.uncork() 將輸出在 stream.cork() 方法被呼叫之後緩衝在記憶體中的所有資料
stream.cork();
stream.write('1');
stream.write('2');
process.nextTick(() => stream.uncork());
複製程式碼
readable
'readable' 事件將在流中有資料可供讀取時才觸發。在某些情況下,為 'readable' 事件新增回撥將會導致一些資料被讀取到內部快取中
const readable = getReadableStreamSomehow();
readable.on('readable', () => {
// 某些資料可讀
});
複製程式碼
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
start:3,
end:8,
encoding:'utf8',
highWaterMark:3
});
rs.on('readable',function () {
console.log('readable');
console.log('rs._readableState.buffer.length',rs._readableState.length);
let d = rs.read(1);
console.log('rs._readableState.buffer.length',rs._readableState.length);
console.log(d);
setTimeout(()=>{
console.log('rs._readableState.buffer.length',rs._readableState.length);
},500)
});
複製程式碼
- 當流資料到達尾部時, 'readable' 事件會觸發。觸發順序在 'end' 事件之前
- 事實上, 'readable' 事件表明流有了新的動態:要麼是有了新的資料,要麼是到了流的尾部。 對於前者, stream.read() 將返回可用的資料。而對於後者, stream.read() 將返回 null。
可讀流的兩種模式
- 可讀流的兩種工作模式: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 時, 資料可能堆積到流的內部快取中。