1. Node.js 中有四種基本的流型別:
- Readable - 可讀的流 (例如 fs.createReadStream()).
- Writable - 可寫的流 (例如 fs.createWriteStream()).
- Duplex - 可讀寫的流 (例如 net.Socket).
- Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate()).
2. 流中的資料有兩種模式,二進位制模式和物件模式.
- 二進位制模式, 每個分塊都是buffer或者string物件.
- 物件模式, 流內部處理的是一系列普通物件.
所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。
3. 可讀流的兩種模式
- 可讀流事實上工作在下面兩種模式之一:
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' 事件監聽,就有可能出現這種情況。
4.快取區
Writable 和 Readable 流都會將資料儲存到內部的緩衝器(buffer)中。這些緩衝器可以 通過相應的 writable._writableState.getBuffer() 或 readable._readableState.buffer 來獲取。
緩衝器的大小取決於傳遞給流建構函式的 highWaterMark 選項。 對於普通的流, highWaterMark 選項指定了總共的位元組數。對於工作在物件模式的流, highWaterMark 指定了物件的總數。
當可讀流的實現呼叫
stream.push(chunk)
方法時,資料被放到緩衝器中。如果流的消費者沒有呼叫stream.read()
方法, 這些資料會始終存在於內部佇列中,直到被消費。當內部可讀緩衝器的大小達到 highWaterMark 指定的閾值時,流會暫停從底層資源讀取資料,直到當前 緩衝器的資料被消費 (也就是說, 流會在內部停止呼叫 readable._read() 來填充可讀緩衝器)。
可寫流通過反覆呼叫 writable.write(chunk) 方法將資料放到緩衝器。 當內部可寫緩衝器的總大小小於 highWaterMark 指定的閾值時, 呼叫 writable.write() 將返回true。 一旦內部緩衝器的大小達到或超過 highWaterMark ,呼叫 writable.write() 將返回 false 。
stream API 的關鍵目標, 尤其對於 stream.pipe() 方法, 就是限制緩衝器資料大小,以達到可接受的程度。這樣,對於讀寫速度不匹配的源頭和目標,就不會超出可用的記憶體大小。
Duplex 和 Transform 都是可讀寫的。 在內部,它們都維護了 兩個 相互獨立的緩衝器用於讀和寫。 在維持了合理高效的資料流的同時,也使得對於讀和寫可以獨立進行而互不影響。
5. 可讀流的三種狀態
在任意時刻,任意可讀流應確切處於下面三種狀態之一:
- 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 時, 資料可能堆積到流的內部快取中。
6.readable
'readable' 事件將在流中有資料可供讀取時觸發。在某些情況下,為 'readable' 事件新增回撥將會導致一些資料被讀取到內部快取中。
const readable = getReadableStreamSomehow();
readable.on('readable', () => {
// 有一些資料可讀了
});
複製程式碼
- 當到達流資料尾部時, 'readable' 事件也會觸發。觸發順序在 'end' 事件之前。
- 事實上, 'readable' 事件表明流有了新的動態:要麼是有了新的資料,要麼是到了流的尾部。 對於前者, stream.read() 將返回可用的資料。而對於後者, stream.read() 將返回 null。
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)
});
複製程式碼
7.流的經典應用
7.1 行讀取器
7.1.1 換行和回車
- 以前的列印要每秒可以列印10個字元,換行城要0.2秒,正要可以列印2個字元。
- 研製人員就是在每行後面加兩個表示結束的字元。一個叫做"回車",告訴打字機把列印頭定位在左邊界;另一個叫做"換行",告訴打字機把紙向下移一行。
- Unix系統裡,每行結尾只有換行"(line feed)",即"\n",
- Windows系統裡面,每行結尾是"<回車><換行>",即"\r\n"
- Mac系統裡,每行結尾是"回車"(carriage return),即"\r"
- 在ASCII碼裡
- 換行 \n 10 0A
- 回車 \r 13 0D
7.1.2 程式碼
let fs = require('fs');
let EventEmitter = require('events');
let util = require('util');
util.inherits(LineReader, EventEmitter)
fs.readFile('./1.txt',function (err,data) {
console.log(data);
})
function LineReader(path) {
EventEmitter.call(this);
this._rs = fs.createReadStream(path);
this.RETURN = 0x0D;// \r 13
this.NEW_LINE = 0x0A;// \n 10
this.on('newListener', function (type, listener) {
if (type == 'newLine') {
let buffer = [];
this._rs.on('readable', () => {
let bytes;
while (null != (bytes = this._rs.read(1))) {
let ch = bytes[0];
switch (ch) {
case this.RETURN:
this.emit('newLine', Buffer.from(buffer));
buffer.length = 0;
let nByte = this._rs.read(1);
if (nByte && nByte[0] != this.NEW_LINE) {
buffer.push(nByte[0]);
}
break;
case this.NEW_LINE:
this.emit('newLine', Buffer.from(buffer));
buffer.length = 0;
break;
default:
buffer.push(bytes[0]);
break;
}
}
});
this._rs.on('end', () => {
if (buffer.length > 0) {
this.emit('newLine', Buffer.from(buffer));
buffer.length = 0;
this.emit('end');
}
})
}
});
}
var lineReader = new LineReader('./1.txt');
lineReader.on('newLine', function (data) {
console.log(data.toString());
}).on('end', function () {
console.log("end");
})複製程式碼