要說node最令人印象深刻的模組,第一肯定是events,而第二肯定就是stream模組了,今天這篇文章就來跟大家聊聊stream的實現,主要是以stream_readable為例來講解,並以fs模組中的createReadStream為例來說明node內部是如何使用stream的。
Stream.Readable
從lib/stream.js中我們可以看到node的stream主要分為了四種,讀流,寫流以及讀寫兼具的全雙工流,還有可以修改和變換資料的 Transform流 ,不過讀流和寫流的實現邏輯和思路在大架構上是類似的,至於雙全工流我們可以通過檔案lib/_stream_duplex.js看到他其實就是通過:
function Duplex(options) {
...
Readable.call(this, options);
Writable.call(this, options);
...
}
複製程式碼
使函式擁有了讀流和寫流所有的方法和屬性而成,而Transform流其實是 Duplex流重寫了_read和_write方法的派生流,所以本文通過介紹讀流的實現方式來理解整個stream的實現方式。
首先,可以從lib/stream.js中發現stream的真實位置來源於lib/internal/streams/legacy.js,從這個檔案中我們可以看到stream也繼承於events,並且這裡還提供了一個pipe方法,這個方法我們後面講例子的時候再講。而從lib/_stream_readable.js檔案中可以看出,雖然提供了能組成整套流程的方法,但是最核心的Readable.prototype._read
中卻是這樣:
this.emit('error', new Error('_read() is not implemented'));
複製程式碼
會直接爆出錯誤,為什麼呢?註釋中已經給名了原因,該函式是一個抽象方法,是不能被直接呼叫的,只能通過其他實際的讀流來繼承該類再重寫這個方法後使用,可以將Stream.Readable理解為一個抽象類,所以我們直接通過一個fs中的使用用例來說明。
fs.createReadStream
下面我們直接從lib/fs.js的fs.createReadStream
方法來看Stream.Readable,這個方法中返回了一個ReadStream例項,而ReadStream是通過util.inherits(ReadStream, Readable);
關聯上了Stream.Readable,在 new ReadStream
的時候會通過new ReadableState
建立大量控制流操作的屬性,後面我們都會講到。現在讓我們來看一個最簡單的例子:
var stream = fs.createReadStream('sample.txt');
stream.on('data', (chunk) => {
console.log('讀取檔案資料:', chunk);
});
複製程式碼
這個例子中,在建立了讀流以後,就只需要註冊events事件,在有資料的時候來觸發就行了,這是怎麼實現的呢?原來在Stream.Readable
中重寫了events的on方法:
Readable.prototype.on = function(ev, fn) {
const res = Stream.prototype.on.call(this, ev, fn);
if (ev === 'data') {
if (this._readableState.flowing !== false)
this.resume();
} else if (ev === 'readable') {
const state = this._readableState;
if (!state.endEmitted && !state.readableListening) {
state.readableListening = state.needReadable = true;
state.emittedReadable = false;
if (!state.reading) {
process.nextTick(nReadingNextTick, this);
} else if (state.length) {
emitReadable(this);
}
}
}
return res;
};
複製程式碼
其中當註冊data和readable事件的時候,我們先以例子中data事件舉例往下分析,這裡呼叫了ReadStream.prototype.resume
函式,這個函式會在最後通過process.nextTick
呼叫函式resume_
,所以我們可以直接來看看這個函式的程式碼:
if (!state.reading) {
debug('resume read 0');
stream.read(0);
}
state.resumeScheduled = false;
state.awaitDrain = 0;
stream.emit('resume');
flow(stream);
if (state.flowing && !state.reading)
stream.read(0);
複製程式碼
其中的state就是ReadableState
的例項,當非同步讀取未完成時reading屬性為true,不過我們的程式碼剛開始為初始值false所以這裡會直接呼叫stream.read(0);
。現在讓我們來看一下Readable.prototype.read
的程式碼,這個方法在這次呼叫時,會直接執行以下的程式碼:
....//忽略做判斷以及不執行的程式碼
state.reading = true;
state.sync = true;
if (state.length === 0)
state.needReadable = true;
this._read(state.highWaterMark);
state.sync = false;
...//忽略做判斷以及不執行的程式碼
if (ret === null) {
state.needReadable = true;
n = 0;
}
複製程式碼
這裡主要就是執行重寫了的ReadStream.prototype._read
,這裡我們可以看到這個方法第一句程式碼:
if (typeof this.fd !== 'number') {
return this.once('open', function() {
this._read(n);
});
}
複製程式碼
這是為了保證檔案已經被開啟了,讓我們回到ReadStream
建構函式中,這裡有這樣一段程式碼:
if (typeof this.fd !== 'number')
this.open();
複製程式碼
當我們只傳了檔案路徑的時候,會直接呼叫ReadStream.prototype.open
這個函式實際就是使用了fs.open
開啟檔案路徑,然後在成功回撥後通過:
self.emit('open', fd);
// start the flow of data.
self.read();
複製程式碼
觸發open事件,並呼叫read方法。瞭解了這個以後,我們接著回來看_read方法:
//分配buffer,校正讀取長度
...
//已經不能繼續讀取了
if (toRead <= 0)
return this.push(null);
fs.read(this.fd, pool, pool.used, toRead, this.pos, (err, bytesRead) => {
//錯誤處理
if(err){
...
}else{
var b = null;
if (bytesRead > 0) {
this.bytesRead += bytesRead;
b = thisPool.slice(start, start + bytesRead);
}
this.push(b);
}
}
//記錄本次讀取後的檔案的偏移
if (this.pos !== undefined)
this.pos += toRead;
複製程式碼
在第一次通過_read成功讀取了資料後會呼叫Readable.prototype.push
,這個方法呼叫addChunk
方法,在這個方法中有這樣一段程式碼:
if (state.flowing && state.length === 0 && !state.sync) {
stream.emit('data', chunk);
stream.read(0);
}
複製程式碼
state.flowing
在之前說的ReadStream.prototype.resume
中已經設定為true了,而state.length
為初始值即為0,因為我們是非同步呼叫所以state.sync
也為false,這個if語句成立後會直接觸發data事件,並將讀取的資料傳遞給回撥方法。然後通過stream.read(0);
不斷的重複讀取,直到讀取不到資料時,在_read的方法中通過this.push(null);
呼叫方法onEofChunk
將讀操作停止掉。而如果以下面的例子會發生什麼不同的情況呢?
var stream = fs.createReadStream('sample.txt');
stream.on('readable', () => {
console.log('檔案資料可讀');
});
複製程式碼
只註冊了readable事件的情況呢?我們回過頭去看之前Readable.prototype.on
的方法中對readable事件的處理,通過分析可以瞭解到會呼叫process.nextTick
方法執行函式nReadingNextTick
,這個函式只有一句話self.read(0);
,到了這裡讀取的邏輯跟之前的邏輯就基本一樣了,但是在Readable.prototype.push
中發生了變化,因為state.flowing
為false,所以就會執行程式碼:
state.length += state.objectMode ? 1 : chunk.length;
if (addToFront)
state.buffer.unshift(chunk);
else
state.buffer.push(chunk);
if (state.needReadable)
emitReadable(stream);
複製程式碼
會將讀取到的buffer存到ReadableState的buffer屬性中,並通過emitReadable
觸發readble事件的回撥。在執行完這些之後,函式也會通過process.nextTick
註冊tick任務,執行如下程式碼:
var len = state.length;
while (!state.reading && !state.flowing && !state.ended &&
state.length < state.highWaterMark) {
stream.read(0);
if (len === state.length)
break;
else
len = state.length;
}
複製程式碼
可以看到當state.flowing
為true的時候並不會執行,所以在上面我們並沒有介紹這個方法,這個方法會通過不斷觸發read一直執行到state中的buffer快取區快取的資料達到highWaterMark所設定的閾值或沒有讀到新資料為止。那假如在這個時候,我們再註冊data事件會怎麼樣呢?這個時候就會觸發我們之前說的resume_
函式,而在其中之前說到的最開頭的呼叫stream.read(0)
因為ReadableState.length
超過了閾值,所以並不會執行_read方法,而是直接跳過,但是在後面的flow(stream)
中通過flow(stream);
呼叫執行程式碼while (state.flowing && stream.read() !== null);
,而這裡的stream.read()
會執行以下語句:
n = parseInt(n, 10);
...
n = howMuchToRead(n, state);
...
if (n > 0)
ret = fromList(n, state);
...
if (ret !== null)
this.emit('data', ret);
return ret;
複製程式碼
其中因為read呼叫沒傳入任何引數,所以parseInt
的結果為NaN,從howMuchToRead
中可以看到:
if (n !== n) {
if (state.flowing && state.length)
return state.buffer.head.data.length;
else
return state.length;
}
複製程式碼
會返回之前儲存在state中的buffer的size,然後通過fromList(n, state)
從state儲存的buffer中取出資料返回,並觸發data事件的回撥。不過一般而言,如果選擇使用註冊readable事件,則是選擇使用手動的方式來讀取內容了。比如:
var stream = fs.createReadStream('sample.txt',{encoding:'utf8'});
stream.on('readable', () => {
console.log('檔案資料為:' + stream.read() );
});
複製程式碼
總結
從上面的描述大家可以瞭解到流中因為屬性多,判斷多,不同的實現有不同的觸發機制所以顯得比較混亂,希望通過上文的講解能讓大家對node的stream的設計思路有所瞭解,本文篇幅有限所以其他比如寫流的實現和pipe的實現大家就可以自己去研究一下了,如果有興趣還能模仿fs的流生成過程,用node的stream派生出自己的stream來做一些嘗試,這裡給大家一個例子參考:
var stream = require('stream');
var util = require('util');
util.inherits(Coustomer, stream.Readable);
function Coustomer(options) {
stream.Readable.call(this, options);
this._str = "";
}
Coustomer.prototype._read = function() {
if(this._str.length < 10){
this._str += "a";
this.push(this._str);
}else{
this.push(null);
}
};
var coustoomer = new Coustomer();
coustoomer.on('data', function(data){
console.log("讀到資料: " + data.toString());//no maybe
});
coustoomer.on('end', function(data){
console.log("結束");
});
複製程式碼