流(stream)是一種在 Node.js 中處理流式資料的抽象介面。 stream 模組提供了一些基礎的 API,用於構建實現了流介面的物件。
Node.js 提供了多種流物件。 例如,傳送到 HTTP 伺服器的請求和 process.stdout 都是流的例項。
流可以是可讀的、可寫的、或是可讀寫的。 所有的流都是 EventEmitter 的例項。
流的型別
Node.js 中有四種基本的流型別(本篇主要說前兩種):
- Writable - 可寫入資料的流(例如 fs.createWriteStream())
- Readable - 可讀取資料的流(例如 fs.createReadStream())
- Duplex - 可讀又可寫的流(例如 net.Socket)
- Transform - 在讀寫過程中可以修改或轉換資料的 Duplex 流(例如 zlib.createDeflate())
緩衝
流中一個相當重要的概念,無論讀寫流都是通過緩衝來實現的。 可寫流和可讀流都會在一個內部的緩衝器中儲存資料,可以分別使用的 writable.writableBuffer 或 readable.readableBuffer 來獲取,可緩衝的資料的數量取決於傳入流建構函式的 highWaterMark 選項,預設情況下highWaterMark 64*1024個位元組 讀寫的過程都是將資料讀取寫入緩衝,然後在將資料讀出或者寫入檔案。
幾個重要的底層方法
- writable.write(chunk[, encoding][, callback]) writable.write() 方法向流中寫入資料,並在資料處理完成後呼叫 callback 。如果有錯誤發生, callback 不一定 以這個錯誤作為第一個引數並被呼叫。要確保可靠地檢測到寫入錯誤,應該監聽 'error' 事件。 在確認了 chunk 後,如果內部緩衝區的大小小於建立流時設定的 highWaterMark 閾值,函式將返回 true 。 如果返回值為 false ,應該停止向流中寫入資料,直到 'drain' 事件被觸發。 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 'drain' 事件就會被觸發。
- readable.read([size])
來一個小例子,有助於理解
// pipe
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
highWaterMark:1
})
let ws = fs.createWriteStream('./5.txt',{
highWaterMark:2
})
let index = 1;
rs.on('data', (data) => {
console.log(index++)
let flag = ws.write(data); // 當內部的可寫緩衝的總大小小於 highWaterMark 設定的閾值時,
//呼叫 writable.write() 會返回 true。 一旦內部緩衝的大小達到或超過 highWaterMark 時,則會返回 false。
if (!flag) { //內部緩衝超過highWaterMark
rs.pause()
}
})
let wsIndex = 1;
ws.on('drain', () => {
console.log('ws'+wsIndex++)
rs.resume()
})
// 1 2 ws1 3 4 ws2 5 6 ws3
複製程式碼
幾個重要的事件監聽
前面已經說了所有的流都是 EventEmitter 的例項,那麼就可以on,可以emit等等
- rs.on('data',()) //讀入緩衝
- ws.on('drain',()) //寫的緩衝被清空
上面的例子中 當寫緩衝大於highWaterMark時 我們就要暫停讀取,等待監聽到drain事件,然後重新啟動rs.resume()讀取
其實啊,在工作中也是很少直接這用到的,我們可以直接用pipe rs.pipe(ws)即可 這樣就給一個可讀流寫入到一個可寫流當中
自己實現的可讀流
let EventEmitter = require('events'); //所有的流都是 EventEmitter 的例項,流繼承EventEmitter
let fs = require('fs');
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.autoClose = options.autoClose || true;
this.flags = options.flags || 'r';
this.encoding = options.encoding || null;
this.start = options.start || 0;
this.end = options.end || null;
this.highWaterMark = options.highWaterMark || 64 * 1024;
// 應該有一個讀取檔案的位置 可變的(可變的位置)
this.pos = this.start;
// 控制當前是否是流動模式
this.flowing = null;
// 構建讀取到的內容的buffer
this.buffer = Buffer.alloc(this.highWaterMark);
// 當建立可讀流 要將檔案開啟
this.open(); // 非同步執行
this.on('newListener', (type) => {
if(type === 'data'){ // 使用者監聽了data事件,就開始讀取吧
this.flowing = true;
this.read();// 開始讀取檔案
}
});
}
read(){
// 這時候檔案還沒有開啟呢,等待著檔案開啟後再去讀取
if(typeof this.fd !== 'number'){
// 等待著檔案開啟,再次呼叫read方法
return this.once('open',()=>this.read());
}
// 開始讀取了
// 檔案可能有10個字串
// start 0 end 4
// 每次讀三個 3
// 0-2
// 34
let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark
// 檔案描述符 讀到哪個buffer裡 讀取到buffer的哪個位置
// 往buffer裡讀取幾個,讀取的位置
fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
if (bytesRead>0){ // 讀到內容了
this.pos += bytesRead;
// 保留有用的
let r = this.buffer.slice(0, bytesRead);
r = this.encoding ? r.toString(this.encoding) : r;
// 第一次讀取
this.emit('data', r);
if (this.flowing) {
this.read();
}
}else{
this.end = true;
this.emit('end');
this.destroy();
}
});
}
destroy() { // 判斷檔案是否開啟 (將檔案關閉掉)
if (typeof this.fd === 'number') {
fs.close(this.fd, () => {
this.emit('close');
});
return;
}
this.emit('close');
}
open() { // 開啟檔案的邏輯
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy(); // 銷燬 關閉檔案(觸發close事件)
} return;
}
this.fd = fd;
this.emit('open'); // 觸發檔案開啟事件
});
}
pause(){
this.flowing = false;
}
resume(){
this.flowing = true;
this.read(); // 繼續讀取
}
}
module.exports = ReadStream;
複製程式碼
自己實現的可寫流
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
constructor(path,options ={}){
super();
this.path = path;
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.highWaterMark = options.highWaterMark || 16*1024;
this.start = options.start || 0;
this.autoClose = options.autoClose|| true;
this.encoding = options.encoding || 'utf8';
// 是否需要觸發drain事件
this.needDrain = false;
// 是否正在寫入
this.writing = false;
// 快取 正在寫入就放到快取中
this.buffer = [];
// 算一個當前快取的個數
this.len = 0;
// 寫入的時候也有位置關係
this.pos = this.start;
this.open();
}
// 0 [1 2]
write(chunk, encoding = this.encoding,callback){
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
this.len += chunk.length;// 每次呼叫write就統計一下長度
this.needDrain = this.highWaterMark <= this.len;
// this.fd
if(this.writing){
this.buffer.push({chunk,encoding,callback});
}else{
// 當檔案寫入後 清空快取區的內容
this.writing = true; // 走快取
this._write(chunk,encoding,()=>this.clearBuffer());
}
return !this.needDrain; // write 的返回值必須是true / false
//這時候可以回頭看一下上面的例子,在this.len >= this.higWaterMark的時候,返回了一個fasle,例子中就暫停讀取了。等待寫入完成
}
_write(chunk,encoding,callback){
if (typeof this.fd !== 'number') {
return this.once('open', () => this._write(chunk, encoding, callback));
}
// fd是檔案描述符 chunk是資料 0 寫入的位置和 長度 , this.pos偏移量
fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{
this.pos += bytesWritten;
this.len -= bytesWritten; // 寫入的長度會減少
callback();
});
}
clearBuffer(){
let buf = this.buffer.shift();
if(buf){
this._write(buf.chunk, buf.encoding, () => this.clearBuffer());
}else{
this.writing = false;
this.needDrain = false; // 觸發一次drain 再置回false 方便下次繼續判斷
this.emit('drain');
}
}
destroy(){
if(typeof this.fd === 'number'){
fs.close(this.fd,()=>{
this.emit('close');
});
return
}
this.emit('close');
}
open(){
fs.open(this.path,this.flags,this.mode,(err,fd)=>{
if(err){
this.emit('error');
this.destroy();
return
}
this.fd = fd;
this.emit('open');
});
}
}
module.exports = WriteStream;
複製程式碼
以上就是流的一些基礎知識,流的簡單應用以及自己實現的可讀流可寫流。當然有很多不足之處,希望朋友們提出指正。也希望和各位朋友一起學習分享!