readStream
流都是基於EventEmitter實現的 我們先看看node自帶的讀流用法:
let fs = require('fs');
// 一般情況下我們不會使用後面的引數
let rs = fs.createReadStream('./1.txt'{
highWaterMark: 3, // 位元組
flags:'r',
autoClose:true, // 預設讀取完畢後自動關閉
start:0,
//end:3,// 流是閉合區間 包start也包end
encoding:'utf8'
});
// 預設建立一個流 是非流動模式,預設不會讀取資料
// 我們需要接收資料 我們要監聽data事件,資料會總動的流出來
rs.on('error',function (err) {
console.log(err)
});
rs.on('open',function () {
console.log('檔案開啟了');
});
// 內部會自動的觸發這個事件 rs.emit('data');,預設情況下會讀完整個檔案
rs.on('data',function (data) {
console.log(data);
rs.pause(); // 暫停觸發on('data')事件,將流動模式又轉化成了非流動模式
});
setTimeout(()=>{rs.resume()},5000)
rs.on('end',function () {
console.log('讀取完畢了');
});
rs.on('close',function () {
console.log('關閉')
});
複製程式碼
接下來手寫writeStream:
先把constructor內部自帶屬性對應好
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.pos = this.start; // pos會隨著讀取的位置改變
this.end = options.end || null; // null表示沒傳遞
this.encoding = options.encoding || null;
this.flags = options.flags || 'r';
this.flowing = null; // 非流動模式
// 用buffer盛讀到的資料
this.buffer = Buffer.alloc(this.highWaterMark);
}
複製程式碼
屬性配好了,結下來開始讀,讀檔案第一步是開啟檔案,先實現open方法
open() {
fs.open(this.path, this.flags, (err, fd) => { //fd標識的就是當前this.path這個檔案,從3開始(number型別)
if (err) {
if (this.autoClose) { // 如果需要自動關閉,就銷燬fd
this.destroy(); // 銷燬(關閉檔案,觸發關閉事件)
}
this.emit('error', err); // 如果有錯誤觸發error事件
return;
}
this.fd = fd; // 儲存檔案描述符
this.emit('open', this.fd); // 觸發檔案的開啟的方法
});
}
複製程式碼
這裡用到了destory方法,它用來關閉檔案
destroy() {
if (typeof this.fd != 'number') { return this.emit('close'); }
fs.close(this.fd, () => {
// 如果檔案開啟過了 那就關閉檔案並且觸發close事件
this.emit('close');
});
}
複製程式碼
接下來最關鍵的來了,readStream 什麼時候開始讀取資料呢?只要註冊了data 事件,就去讀檔案。如何監測data事件呢?用到了EventEmitter的newListener事件,該事件是同步的,只要註冊了其它非newListener型別的事件,就會觸發newListener型別事件對應的回撥函式,可以在回撥函式中讀檔案
this.on('newListener', (type) => { // 等待著 它監聽data事件
if (type === 'data') {
this.flowing = true;
this.read();// 開始讀取 客戶已經監聽了data事件
}
})
複製程式碼
接下來整理下constructor:
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.pos = this.start; // pos會隨著讀取的位置改變
this.end = options.end || null; // null表示沒傳遞
this.encoding = options.encoding || null;
this.flags = options.flags || 'r';
// 引數的問題
this.flowing = null; // 非流動模式
// 弄一個buffer讀出來的數
this.buffer = Buffer.alloc(this.highWaterMark);
this.open();
// 次方法預設同步呼叫的
this.on('newListener', (type) => { // 等待著 它監聽data事件
if (type === 'data') {
this.flowing = true;
this.read();// 開始讀取 客戶已經監聽了data事件
}
})
}
}
複製程式碼
那如何讀取檔案呢?上面程式碼中,我們注意到在newListener事件回撥中呼叫了read 方法,由於該事件是同步的,有可能在檔案還沒開啟時,就 註冊了data事件--->觸發了newListener事件--> 然後去讀檔案,此時如果繼續讀,肯定讀取失敗。因此需要處理未開啟的情況。
read(){
// 預設第一次呼叫read方法時還沒有獲取fd,所以不能直接讀
if(typeof this.fd !== 'number'){
return this.once('open',() => this.read()); // 等待著觸發open事件後fd肯定拿到了,拿到以後再去執行read方法
}
// 當獲取到fd時 開始讀取檔案了
// 第一次應該讀2個 第二次應該讀2個
// 第二次pos的值是4 end是4
// 一共4個數 123 4
let howMuchToRead = this.end ?Math.min(this.end-this.pos+1,this.highWaterMark): this.highWaterMark;
fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (error, byteRead) => {
// byteRead真實的讀到了幾個
this.pos += byteRead; // 位置偏移
// this.buffer預設就是三個
let b = this.encoding ? this.buffer.slice(0, byteRead).toString(this.encoding) : this.buffer.slice(0, byteRead);
this.emit('data', b);
if ((byteRead === this.highWaterMark)&&this.flowing){
return this.read(); // 繼續讀
}
// 這裡就是沒有更多的邏輯了
if (byteRead < this.highWaterMark){
// 沒有更多了
this.emit('end'); // 讀取完畢
this.destroy(); // 銷燬即可
}
});
}
複製程式碼
暫停和復位模式:
pause(){
this.flowing = false;
}
resume(){
this.flowing = true;
this.read();
}
複製程式碼
writeStream 流的實現
node自帶的寫流的例子
let fs = require('fs');
let ws = fs.createWriteStream('2.txt',{
flags:'w',
encoding:'utf8',
start:0,
highWaterMark:3 // 一次能寫三個
});
let i = 9;
function write() {
let flag = true; // 表示是否能寫入
while (flag&&i>=0) { // 9 - 0
flag = ws.write(i--+'');
}
}
// drain寫入總數)大於highWaterMark,並且將它們都寫入時才觸發
ws.on('drain',()=>{
console.log('幹了');
write();
})
write();
複製程式碼
接下來手寫writeStream:
先把constructor內部自帶屬性對應好,this.len是當前待寫入的總長度,實際上就是快取區的長度(快取區分兩部分)
class WriteStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || 'w';
this.encoding = options.encoding || 'utf8';
this.start = options.start || 0;
this.pos = this.start;
this.mode = options.mode || 0o666;
this.autoClose = options.autoClose || true;
this.highWaterMark = options.highWaterMark || 16 * 1024;
// fd 非同步的 觸發一個open事件當觸發open事件後fd肯定就存在了
this.open();
// 第一次寫入是真的往檔案裡寫
this.writing = false; // 預設第一次就不是正在寫入
// 快取我用簡單的陣列來模擬一下
this.cache = [];
// 維護一個變數 表示快取的長度
this.len = 0;
// 是否觸發drain事件
this.needDrain = false;
}
}
複製程式碼
open方法以及destory方法讀流一樣:
destroy() {
if (typeof this.fd != 'number') {
this.emit('close');
} else {
fs.close(this.fd, () => {
this.emit('close');
});
}
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy(); // 如果自動關閉就銷燬檔案描述符
}
return;
}
this.fd = fd;
this.emit('open', this.fd);
});
}
複製程式碼
做好了前期工作,當呼叫write時,需要呼叫ws.write方法,接下來寫write方法。記住一點:當我們
寫檔案時,檔案依然可能沒有開啟,需要判斷。
另外這裡需要判斷是否可以觸發drain,當this.len >= this.highWaterMark(快取區長度大於水位線時,才有可能觸發drain)
// 客戶呼叫的是write方法去寫入內容
write(chunk, encoding = this.encoding) {
// 要判斷 chunk必須是buffer或者字串 為了統一,如果傳遞的是字串也要轉成buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
this.len += chunk.length; // 維護快取的長度 3
let ret = this.len < this.highWaterMark;
// 表示需要觸發drain事件,當把this.cache中的資料都寫入時,觸發drain
if (!ret) {
this.needDrain = true;
}
// 正在寫入應該放到記憶體中,
if (this.writing) {
this.cache.push({
chunk,
encoding,
});
} else { // 第一次,//只有第一次呼叫寫入檔案,其餘的寫入this.cache。這裡的第一次還包括緩//存清空後的第一次
this.writing = true;
this._write(chunk, encoding, () => this.clearBuffer()); // 專門實現寫的方法
}
return ret; // 能不能繼續寫了,false表示下次的寫的時候就要佔用更多記憶體了
}
複製程式碼
只要ret返回false,就說明不能繼續寫了,快取區總長度大於水位線了,需要清快取啦!! 重點來了,寫檔案,清快取方法 this._write();
_write(chunk, encoding, clearBuffer) { //因為write方法是同步呼叫的此時fd還沒有獲取到,所以等待獲取到再執行write操作
if (typeof this.fd != 'number') {
return this.once('open', () => this._write(chunk, encoding, clearBuffer));
}
fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, byteWritten) => {
this.pos += byteWritten;
this.len -= byteWritten; // 每次寫入後就要再記憶體中減少一下
clearBuffer(); // 第一次就寫完了
})
}
clearBuffer() {
let buffer = this.cache.shift();
// 快取裡有
if (buffer) {
this._write(buffer.chunk, buffer.encoding, () => this.clearBuffer());
} else {// 快取裡沒有了
if (this.needDrain) { // 需要觸發drain事件
this.writing = false; // 告訴下次直接寫就可以了 不需要寫到記憶體中了
this.needDrain = false;
this.emit('drain');
}
}
}
複製程式碼
好,上面基本的流程已經具備了,再額外加一個pipe方法: rs.pipe(ws);咋實現呢
再readStream中新增如下方法:
pipe(dest){
this.on('data',(data)=>{
let flag = dest.write(data);
if(!flag){
// 已經不能繼續寫了,等寫完了在恢復
this.pause();
}
});
dest.on('drain',()=>{
console.log('寫一下停一下');
// 恢復,繼續寫
this.resume();
});
}
複製程式碼