可寫流(Writable Stream)
可寫流是對資料寫入'目的地'的一種抽象。
可寫流的原理其實與可讀流類似,當資料過來的時候會寫入快取池,當寫入的速度很慢或者寫入暫停時候,資料流便會進入到佇列池快取起來,當然即使快取池滿了,剩餘的資料也是存在記憶體
可寫流的簡單用法如下程式碼
let fs = require('fs');
let path = require('path');
let ws = fs.createWriteStream(path.join(__dirname,'1.txt'),{
highWaterMark:3,
autoClose:true,
flags:'w',
encoding:'utf8',
mode:0o666,
start:0,
});
let i = 9;
function write(){
let flag = true;
while(i>0&&flag){
flag = ws.write(--i+'','utf8',()=>{console.log('ok')});
console.log(flag)
}
}
write();
// drain只有當快取區充滿後 並且被消費後觸發
ws.on('drain',function(){
console.log('抽乾')
write();
});
複製程式碼
實現原理
現在就讓我們來實現一個簡單的可寫流,來研究可寫流的內部原理,可寫流有很多方法與可讀流類似,這裡不在重複了首先要有一個建構函式來定義一些基本選項屬性,然後呼叫一個open放法開啟檔案,並且有一個destroy方法來處理關閉邏輯
let EventEmitter = require('events');
let fs = require('fs');
class WriteStream extends EventEmitter {
constructor(path,options) {
super();
this.path = path;
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.autoClose = options.autoClose || true;
this.mode = options.mode;
this.start = options.start || 0;
this.flags = options.flags || 'w';
this.encoding = options.encoding || 'utf8';
// 可寫流 要有一個快取區,當正在寫入檔案是,內容要寫入到快取區中
// 在原始碼中是一個連結串列 => []
this.buffers = [];
// 標識 是否正在寫入
this.writing = false;
// 是否滿足觸發drain事件
this.needDrain = false;
// 記錄寫入的位置
this.pos = 0;
// 記錄快取區的大小
this.length = 0;
this.open();
}
destroy() {
if (typeof this.fd !== 'number') {
return this.emit('close');
}
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');
})
}
}
module.exports = WriteStream;
複製程式碼
接著我們實現write方法來讓可寫流物件呼叫,在write方法中我們首先將資料轉化為buffer,接著實現一些事件的觸發條件的邏輯,如果現在沒有正在寫入的話我們就要真正的進行寫入操作了,這裡我們實現一個_write方法來實現寫入操作,否則則代表檔案正在寫入,那我們就將流傳來的資料先放在快取區中,保證寫入資料不會同時進行。
write(chunk,encoding=this.encoding,callback=()=>{}){
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding);
// write 返回一個boolean型別
this.length+=chunk.length;
let ret = this.length<this.highWaterMark; // 比較是否達到了快取區的大小
this.needDrain = !ret; // 是否需要觸發needDrain
// 判斷是否正在寫入 如果是正在寫入 就寫入到快取區中
if(this.writing){
this.buffers.push({
encoding,
chunk,
callback
}); // []
}else{
// 專門用來將內容 寫入到檔案內
this.writing = true;
this._write(chunk,encoding,()=>{
callback();
this.clearBuffer();
}); // 8
}
return ret;
}
_write(chunk,encoding,callback){
if(typeof this.fd !== 'number'){
return this.once('open',()=>this._write(chunk,encoding,callback));
}
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
this.length -= byteWritten;
this.pos += byteWritten;
callback(); // 清空快取區的內容
});
}
複製程式碼
_write寫入之後的回撥中我們會呼叫傳入回撥函式clearBuffer,這個方法會去buffers中繼續遞迴地把資料取出,然後繼續呼叫_write方法去寫入,直到全部buffer中的資料取出後,這樣就清空了buffers。
clearBuffer(){
let buffer = this.buffers.shift();
if(buffer){
this._write(buffer.chunk,buffer.encoding,()=>{
buffer.callback();
this.clearBuffer()
});
}else{
this.writing = false;
if(this.needDrain){ // 是否需要觸發drain 需要就發射drain事件
this.needDrain = false;
this.emit('drain');
}
}
}
複製程式碼
最後附上完整的程式碼
let EventEmitter = require('events');
let fs = require('fs');
class WriteStream extends EventEmitter{
constructor(path,options){
super();
this.path = path;
this.highWaterMark = options.highWaterMark||16*1024;
this.autoClose = options.autoClose||true;
this.mode = options.mode;
this.start = options.start||0;
this.flags = options.flags||'w';
this.encoding = options.encoding || 'utf8';
// 可寫流 要有一個快取區,當正在寫入檔案是,內容要寫入到快取區中
// 在原始碼中是一個連結串列 => []
this.buffers = [];
// 標識 是否正在寫入
this.writing = false;
// 是否滿足觸發drain事件
this.needDrain = false;
// 記錄寫入的位置
this.pos = 0;
// 記錄快取區的大小
this.length = 0;
this.open();
}
destroy(){
if(typeof this.fd !=='number'){
return this.emit('close');
}
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');
})
}
write(chunk,encoding=this.encoding,callback=()=>{}){
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding);
// write 返回一個boolean型別
this.length+=chunk.length;
let ret = this.length<this.highWaterMark; // 比較是否達到了快取區的大小
this.needDrain = !ret; // 是否需要觸發needDrain
// 判斷是否正在寫入 如果是正在寫入 就寫入到快取區中
if(this.writing){
this.buffers.push({
encoding,
chunk,
callback
}); // []
}else{
// 專門用來將內容 寫入到檔案內
this.writing = true;
this._write(chunk,encoding,()=>{
callback();
this.clearBuffer();
}); // 8
}
return ret;
}
clearBuffer(){
let buffer = this.buffers.shift();
if(buffer){
this._write(buffer.chunk,buffer.encoding,()=>{
buffer.callback();
this.clearBuffer()
});
}else{
this.writing = false;
if(this.needDrain){ // 是否需要觸發drain 需要就發射drain事件
this.needDrain = false;
this.emit('drain');
}
}
}
_write(chunk,encoding,callback){
if(typeof this.fd !== 'number'){
return this.once('open',()=>this._write(chunk,encoding,callback));
}
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
this.length -= byteWritten;
this.pos += byteWritten;
callback(); // 清空快取區的內容
});
}
}
module.exports = WriteStream;
複製程式碼
Pipe管道流
前面我們瞭解了可讀流與可寫流,那麼怎麼讓二者結合起來使用呢,node給我們提供好了方法--Pipe管道,流顧名思義,就是在可讀流與可寫流中間加入一個管道,實現一邊讀取,一邊寫入,讀一點寫一點。
Pipe的使用方法如下
let fs = require('fs');
let path = require('path');
let ReadStream = require('./ReadStream');
let WriteStream = require('./WriteStream');
let rs = new ReadStream(path.join(__dirname, './1.txt'), {
highWaterMark: 4
});
let ws = new WriteStream(path.join(__dirname, './2.txt'), {
highWaterMark: 1
});
// 4 1
rs.pipe(ws);
複製程式碼
實現原理
Pipe的原理比較簡單,簡單說監聽可讀流的data事件來持續獲取檔案中的資料,然後我們就會去呼叫寫流的write方法。如果可寫流快取區已滿,那麼當我們得到呼叫可讀流的pause方法來暫停讀取,然後等到寫流的快取區已經全部寫入並且觸發drain事件時,我們就會呼叫resume重新開啟讀取的流程。上程式碼
pipe(ws) {
this.on('data', (chunk) => {
let flag = ws.write(chunk);
if (!flag) {
this.pause();
}
});
ws.on('drain', () => {
this.resume();
})
}
複製程式碼
自定義流
Node允許我們自定義流,讀流繼承於Readable介面,寫流則繼承於Writable介面,所以我們其實是可以自定義一個流模組,只要繼承stream模組對應的介面即可。
自定義可讀流
如果我們要自定義讀流的話,那我們就需要繼承Readable,Readable裡面有一個read()方法,預設呼叫_read(),所以我們只要複寫了_read()方法就可實現讀取的邏輯,同時Readable中也提供了一個push方法,呼叫push方法就會觸發data事件,push中的引數就是data事件回撥函式的引數,當push傳入的引數為null的時候就代表讀流停止,上程式碼
let { Readable } = require('stream');
// 想實現什麼流 就繼承這個流
// Readable裡面有一個read()方法,預設掉_read()
// Readable中提供了一個push方法你呼叫push方法就會觸發data事件
let index = 9;
class MyRead extends Readable {
_read() {
// 可讀流什麼時候停止呢? 當push null的時候停止
if (index-- > 0) return this.push('123');
this.push(null);
}
}
let mr = new MyRead();
mr.on('data', function(data) {
console.log(data);
});
複製程式碼
自定義可寫流
與自定義讀流類似,自定義寫流需要繼承Writable介面,並且實現一個_write()方法,這裡注意的是_write中可以傳入3個引數,chunk, encoding, callback,chunk就是代表寫入的資料,通常是一個buffer,encoding是編碼型別,通常不會用到,最後的callback要注意,它並不是我們用這個自定義寫流呼叫write時的回撥,而是我們上面講到寫流實現時的clearBuffer函式。
let { Writable } = require('stream');
// 可寫流實現_write方法
// 原始碼中預設呼叫的是Writable中的write方法
class MyWrite extends Writable {
_write(chunk, encoding, callback) {
console.log(chunk.toString());
callback(); // clearBuffer
}
}
let mw = new MyWrite();
mw.write('111', 'utf8', () => {
console.log(1);
})
mw.write('222', 'utf8', () => {
console.log(1);
});
複製程式碼
Duplex 雙工流
雙工流其實就是結合了上面我們說的自定義讀流和自定義寫流,它既能讀也能寫,同時可以做到讀寫之間互不干擾
let { Duplex } = require('stream');
// 雙工流 又能讀 又能寫,而且讀取可以沒關係(互不干擾)
let d = Duplex({
read() {
this.push('hello');
this.push(null);
},
write(chunk, encoding, callback) {
console.log(chunk);
callback();
}
});
d.on('data', function(data) {
console.log(data);
});
d.write('hello');
複製程式碼
Transform 轉換流
轉換流的本質就是雙工流,唯一不同的是它並不需要像上面提到的雙工流一樣實現read和write,它只需要實現一個transform方法用於轉換
let { Transform } = require('stream');
// 它的引數和可寫流一樣
let tranform1 = Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase()); // 將輸入的內容放入到可讀流中
callback();
}
});
let tranform2 = Transform({
transform(chunk, encoding, callback){
console.log(chunk.toString());
callback();
}
});
// 等待你的輸入
// rs.pipe(ws);
// 希望將輸入的內容轉化成大寫在輸出出來
process.stdin.pipe(tranform1).pipe(tranform2);
// 物件流 可讀流裡只能放buffer或者字串 物件流裡可以放物件
複製程式碼
物件流
預設情況下,流處理的資料是Buffer/String型別的值。物件流的特點就是它有一個objectMode標誌,我們可以設定它讓流可以接受任何JavaScript物件。上程式碼
const { Transform } = require('stream');
let fs = require('fs');
let rs = fs.createReadStream('./users.json');
rs.setEncoding('utf8');
let toJson = Transform({
readableObjectMode: true,
transform(chunk, encoding, callback) {
this.push(JSON.parse(chunk));
callback();
}
});
let jsonOut = Transform({
writableObjectMode: true,
transform(chunk, encoding, callback) {
console.log(chunk);
callback();
}
});
rs.pipe(toJson).pipe(jsonOut);
複製程式碼