上篇文章寫了可讀流的用法和原始碼實現,可以先去看下,其中有相似的地方,重複的地方就不多做介紹了,就直接寫用法。有一點值得一提就是可寫流有快取區的概念
可寫流的用法
建立可寫流
- 詳細的預設引數配置,貼下官網說明,與可讀流相似,直接貼程式碼
let fs = require('fs');
let ws = fs.createWriteStream('2.txt', {
flags: 'w', // 檔案的操作, 'w'寫入檔案,不存在則建立
mode: 0o666,
autoClose: true,
highWaterMark: 3, // 預設寫是16*1024
encoding: 'utf8'
});
複製程式碼
ws.write()方法和drain事件
- 每次寫入後會有一個標識 flag,當寫入的內容的長度超過highWaterMark就會返回false
- true表示可以繼續寫入,如果返回false,表示快取區滿了,我們應當停止讀取資料以避免消耗過多記憶體。
- 快取區滿後,檔案寫入一直在進行,不一會兒會把快取區的內容全部寫入,快取區處於清空狀態,這時會觸發可寫流的‘drain’事件
不太明白?沒關係,我們看下面這個例子幫助我們理解
let fs = require("fs");
let ws = fs.createWriteStream('1.txt',{
flags:'w',
encoding:'utf8',
start:0,
highWaterMark:3
});
let i =9;
function write() {
let flag = true;
while (flag && i>=0){
flag = ws.write(i-- +'');//往1.txt寫入9876543210
console.log(flag);
}
}
ws.on('drain',()=>{ //快取區充滿並被寫入完成,處於清空狀態時觸發
console.log("幹了");
write(); //當快取區清空後我們在繼續寫
})
write(); //第一次呼叫write方法
複製程式碼
看到這裡,我們應該基本明白用法,下面我們開始寫原始碼實現,鑑於與可讀流有很多相似寫法上篇文章已詳細寫過,重複的就不多說了,說幹就幹
可寫流實現原理
1、宣告WriteStream的建構函式(準備工作)
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.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;
//第一次寫入是真的往檔案裡寫
this.writing = false; //預設第一次不是正在寫入
// 可寫流 要有一個快取區,當正在寫入檔案時,內容要寫入到快取區
this.cache = [];
// 記錄快取區大小
this.len =0;
// 是否觸發drain事件
this.needDrain = false;
this.open(); //目的是拿到fd 非同步,觸發一個open事件後fd肯定存在啦
}
}
複製程式碼
- 把變數宣告好,下面用到的時候就知道怎麼回事了
- 值得主要的是,我們第一次往檔案裡寫入的時候是真的往檔案寫,下次要寫的內容先放到快取區,寫入完成後從快取區去取內容
2、實現open方法和destroy方法
- 這兩個方法與可讀流一樣,不做介紹,直接上程式碼
open(){
fs.open(this.path,this.flags,this.mode,(err,fd)=>{
if(err){
this.emit('error', err); //開啟檔案發生錯誤,釋出error事件
this.emit('error');
if (this.autoClose) { //如果需要自動關閉我再去銷燬fd
this.destroy(); //關閉檔案(觸發關閉事件)
}
return;
}
this.fd = fd; //儲存檔案描述符
this.emit('open', this.fd) //觸發檔案open方法
})
}
destroy() {
if (typeof this.fd != 'number') { //檔案未開啟,也要關閉檔案且觸發close事件
return this.emit('close');
}
fs.close(this.fd, () => { //如果檔案開啟過了 那就關閉檔案並且觸發close事件
this.emit("close");
})
}
複製程式碼
3、實現write方法,與客戶端呼叫ws.write()方法對應
write(chunk,encoding = this.encoding,callback){ //客戶呼叫的是write
//chunk必須是buffer或者字串, 為了統一,如果傳遞的是字串也要轉成buffer
chunk = Buffer.isBuffer(chunk)?chunk : Buffer.from(chunk,encoding);
this.len +=chunk.length; //維護寫入的快取的長度
let ret = this.len <this.highWaterMark; //一個標識 比較是否達成了快取區的大小
this.needDrain = !ret; //是否需要觸發needDrain
if(this.writing){ //預設為false上面定義的,判斷是否正在寫入 如果是正在寫入 就寫入到快取區中
this.cache.push({chunk,encoding,callback})
}else { //第一次寫
this.writing = true;
this._write(chunk,encoding,()=>this.clearBuffer()); //專門實現寫的方法
}
return ret; //能不能繼續寫了 false表示下次寫的時候就要佔用記憶體
}
複製程式碼
- _write()方法 重點, 我們專門用來實現真正的寫入方法
4 、實現重頭戲 _write()方法
_write(chunk,encoding,clearBuffer){
if(typeof this.fd!= 'number'){
//因為write方法是同步呼叫,此時fd還沒有獲取到,所以等待獲取到再執行write
return this.once('open',()=>this._write(chunk,encoding,clearBuffer));
}
//確保有fd
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
this.pos +=byteWritten; //偏移量,預設為0
this.len -=byteWritten; //每次寫入後就要在記憶體中減少下
this.writing = false; //正在寫入就不要再寫拉,放快取區
clearBuffer(); //清空快取區
})
}
複製程式碼
5、清空快取區
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');
}
}
複製程式碼
- 分析下
- 如果是正在寫入,就先把內容放到快取區裡,就是this.cache,預設[]
- 給陣列裡存入一個物件,分別對應chunk, encoding, callback即(()=>this.clearBuffer())
- 每一次寫完後都需要把cache(快取區)裡的內容清空掉
- 當快取區cache陣列裡是空的時候就會觸發drain事件了
寫到這裡,基本就寫完了,原始碼有3000多行,這個只是簡單實現,看不太懂的時候就多寫幾遍(ps 我也寫了好多遍)。測試下看行不行吧