流
Node.js的流就是為了在有限的記憶體中實現我們操作"海量"資料的目標。
流是一組有序的,有起點和終點的位元組資料傳輸手段,它是一個抽象的介面,被 Node 中的很多物件所實現。node裡很多內容都應用到流,比如HTTP 伺服器request和response物件都是流。
它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理。
Node.js中Stream 有四種流型別。
可讀流(Readable)
可讀流分為:
流動模式(flowing mode)
和暫停模式(paused mode)
可讀流在建立時都是暫停模式。暫停模式和流動模式可以互相轉換。
1) 流動模式(flowing mode)流動模式下,資料會源源不斷地生產出來,形成“流動”現象。監聽流的data事件便可進入該模式。
2) 暫停模式(paused mode)暫停模式下,需要顯示地呼叫read(),觸發data事件。
在初始狀態下,監聽data事件,會使流進入流動模式。但如果在暫停模式下,監聽data事件並不會使它進入流動模式。為了消耗流,需要顯示呼叫read()方法。
3)相互轉化
如果不存在管道目標,呼叫readable.resume()可使流進入流動模式
如果存在管道目標,呼叫 stream.unpipe()並取消'data'事件監聽
建立可讀流
var rs = fs.createReadStream(path,[options]);複製程式碼
path讀取檔案的路徑
options
flags 開啟檔案要做的操作,預設為'r'讀取
encoding 預設為null,代表buffer。如果指定utf8編碼highWaterMark要大於3個位元組
start 開始讀取的索引位置
end 結束讀取的索引位置(包括結束位置)
highWaterMark 讀取快取區預設的大小64kb
autoClose 讀取完畢後是否自動關閉
相關方法
flowing流動模式
let fs=require('fs');
let path=require('path');
let rs=fs.createReadStream(path.join(__dirname,'1.txt'),{ //這裡的引數一般不會寫
flags:'r',//檔案的操作是讀取操作
encoding:'utf8', // 預設是null null代表的是buffer
autoClose:true, // 讀取完畢後自動關閉
highWaterMark:3,// 預設是64k 64*1024b
start:0, //讀取的起始位置
end:3 // 讀取的結束位置,包前又包後,相當於閉區間
})
//預設情況下 不會將檔案中的內容輸出
//內部會先建立一個buffer先讀取3b
//相當於有蓋子的水管,不會流出來,儲存在管中
//有兩種模式 非流動模式/暫停模式
//因為建立時第二個引數一般不會寫,讀出來的型別是buffer,這個方法可以指定編碼
rs.setEncoding('utf8');
//開啟檔案
rs.on('open',function(data){
console.log(data)
})
//關閉檔案
rs.on('close',function(data){
console.log(data)
})
//有錯誤就會報錯誤
rs.on('err',function(data){
console.log(data)
})
//暫停模式->流動模式
//流動模式只要監聽了會瘋狂的觸發data事件,直到讀取完畢
rs.on('data',function(data){
console.log(data);
//一開啟水龍頭就嘩嘩出水,有個方法可以讓它暫停
rs.pause(); //暫停方法,表示暫停讀取,暫停data事件觸發
})
setInterval(function(){
rs.resume(); //恢復data事件的觸發,變為流動模式繼續讀取
},3000)
rs.on('end',function(data){ //先end再close關閉
console.log(data)
})
複製程式碼
paused暫停模式
let fs=require('fs');
let path=require('path');
let rs=fs.createReadStream(path.join(__dirname,'1.txt'));
rs.setEncoding('utf8');
// 當我只要建立一個流,就會先把快取區填滿,等待著你自己消費
// 如果當前快取區被清空後會再次觸發readable事件
// 當你消費小於最高水位線時,會自動新增highWater這麼多資料
rs.on('readable', () => {
let d = rs.read(1)
console.log(d)
})複製程式碼
實現可讀流功能原理
流動模式
let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
constructor(path,options){
super();
this.path = path;
this.flags = options.flags || 'r';
this.autoClose = options.autoClose || true;
this.highWaterMark = options.highWaterMark|| 64*1024;
this.start = options.start||0;
this.end = options.end;
this.encoding = options.encoding || null
this.open();//開啟檔案 fd
this.flowing = null; // null就是暫停模式
// 看是否監聽了data事件,如果監聽了 就要變成流動模式
// 要建立一個buffer 這個buffer就是要一次讀多少
this.buffer = Buffer.alloc(this.highWaterMark);
this.pos = this.start; // pos 讀取的位置 可變 start不變的
this.on('newListener',(eventName,callback)=>{
if(eventName === 'data'){
// 相當於使用者監聽了data事件
this.flowing = true;
// 監聽了 就去讀
this.read(); // 去讀內容了
}
})
}
read(){
// 此時檔案還沒開啟呢
if(typeof this.fd !== 'number'){
// 當檔案真正開啟的時候 會觸發open事件,觸發事件後再執行read,此時fd肯定有了
return this.once('open',()=>this.read())
}
// 此時有fd了
let howMuchToRead = this.end?Math.min(this.highWaterMark,this.end-this.pos+1):this.highWaterMark;
fs.read(this.fd,this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
// 讀到了多少個 累加
if(bytesRead>0){
this.pos+= bytesRead;
let data = this.encoding?this.buffer.slice(0,bytesRead).toString(this.encoding):this.buffer.slice(0,bytesRead);
this.emit('data',data);
// 當讀取的位置 大於了末尾 就是讀取完畢了
if(this.pos > this.end){
this.emit('end');
this.destroy();
}
if(this.flowing) { // 流動模式繼續觸發
this.read();
}
}else{
this.emit('end');
this.destroy();
}
});
}
resume(){
this.flowing = true;
this.read();
}
pause(){
this.flowing = false;
}
destroy(){
// 先判斷有沒有fd 有關閉檔案 觸發close事件
if(typeof this.fd ==='number'){
fs.close(this.fd,()=>{
this.emit('close');
});
return;
}
this.emit('close'); // 銷燬
};
open(){
// copy 先開啟檔案
fs.open(this.path,this.flags,(err,fd)=>{
if(err){
this.emit('error',err);
if(this.autoClose){ // 是否自動關閉
this.destroy();
}
return;
}
this.fd = fd; // 儲存檔案描述符
this.emit('open'); // 檔案開啟了
});
}
}
module.exports = ReadStream;複製程式碼
pipe
.pipe()
函式是接受一個源頭src
並將資料輸出到一個可寫的流dst
中簡單來說,邊讀邊寫東西,讀太快,來不及寫,就先暫停讀,等寫完了再繼續讀。
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
});
// 讀四個,寫一個
rs.pipe(ws); // pipe就是讀一點寫一點複製程式碼
pipe原理實現,寫在ReadStream的方法中
pipe(ws){
this.on('data',(chunk)=>{
let flag = ws.write(chunk);
if(!flag){
this.pause();
}
});
ws.on('drain',()=>{
this.resume();
})
}複製程式碼
暫停模式
let fs = require('fs');
let EventEmitter = require('events');
//當讀取內容大於快取區,重新計算讀取數量n的大小的方法
function computeNewHighWaterMark(n) {
n--;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n++;
return n;
}
class ReadStream extends EventEmitter {
constructor(path, options) {
super();
this.path = path;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.autoClose = options.autoClose || true;
this.start = 0;
this.end = options.end;
this.flags = options.flags || 'r';
this.buffers = []; // 快取區
this.pos = this.start;
this.length = 0; // 快取區大小
this.emittedReadable = false;
this.reading = false; // 不是正在讀取的
this.open();
this.on('newListener', (eventName) => {
if (eventName === 'readable') {
this.read();
}
})
}
read(n) { // 想取1個
if(n>this.length){
// 更改快取區大小 讀取五個就找 2的幾次放最近的
this.highWaterMark = computeNewHighWaterMark(n)
this.emittedReadable = true;
this._read();
}
// 如果n>0 去快取區中取吧
let buffer=null;
let index = 0; // 維護buffer的索引的
let flag = true;
if (n > 0 && n <= this.length) { // 讀的內容 快取區中有這麼多
// 在快取區中取 [[2,3],[4,5,6]]
buffer = Buffer.alloc(n); // 這是要返回的buffer
let buf;
while (flag&&(buf = this.buffers.shift())) {
for (let i = 0; i < buf.length; i++) {
buffer[index++] = buf[i];
if(index === n){ // 拷貝夠了 不需要拷貝了
flag = false;
this.length -= n;
let bufferArr = buf.slice(i+1); // 取出留下的部分
// 如果有剩下的內容 在放入到快取中
if(bufferArr.length > 0){
this.buffers.unshift(bufferArr);
}
break;
}
}
}
}
// 當前快取區 小於highWaterMark時在去讀取
if (this.length == 0) {
this.emittedReadable = true;
}
if (this.length < this.highWaterMark) {
if(!this.reading){
this.reading = true;
this._read(); // 非同步的
}
}
return buffer
}
// 封裝的讀取的方法
_read() {
// 當檔案開啟後在去讀取
if (typeof this.fd !== 'number') {
return this.once('open', () => this._read());
}
// 上來我要喝水 先倒三升水 []
let buffer = Buffer.alloc(this.highWaterMark);
fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, bytesRead) => {
if (bytesRead > 0) {
// 預設讀取的內容放到快取區中
this.buffers.push(buffer.slice(0, bytesRead));
this.pos += bytesRead; // 維護讀取的索引
this.length += bytesRead;// 維護快取區的大小
this.reading = false;
// 是否需要觸發readable事件
if (this.emittedReadable) {
this.emittedReadable = false; // 下次預設不觸發
this.emit('readable');
}
} else {
this.emit('end');
this.destroy();
}
})
}
destroy() {
if (typeof this.fd !== 'number') {
return this.emit('close')
}
fs.close(this.fd, () => {
this.emit('close')
})
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy();
}
return
}
this.fd = fd;
this.emit('open');
});
}
}
module.exports = ReadStream;複製程式碼
可寫流(Writeable)
建立可寫流
var ws = fs.createWriteStream(path,[options]);
path寫入的檔案路徑
options
flags開啟檔案要做的操作,預設為'w'
encoding預設為utf8
highWaterMark寫入快取區的預設大小16kb
相關方法
let fs=require('fs');
let path=require('path');
//寫的時候檔案不存在,會建立檔案
let ws = fs.createWriteStream('./1.txt',{
flags:'w',
mode:0o666,
autoClose:true,
highWaterMark:3, // 預設寫是16k
encoding:'utf8',
start:0
});
//第一個引數寫入的資料必須是字串或者Buffer
//第二個引數寫入以什麼編碼寫進去
//第三個引數callback
//有返回值,代表是否能繼續寫,寫的時候,有個快取區的概念。但是返回false,也不會丟失,就是會把內容放到記憶體中
let flag=ws.write(1+'','utf8',()=>{})//這是非同步的方法
//傳入的引數,寫完後也會寫入檔案內
ws.end('ok'); //當寫完後,就不能再繼續寫了
//抽乾方法,當寫入完後會觸發drain方法
//快取區必須滿了,滿了清空後才會觸發drain
//如果呼叫end後,再呼叫這個方法沒有意義了
ws.on('drain',function(){
console.log('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;複製程式碼
啊~~文章似乎太長太囉嗦了,看來怎麼把給自己看的筆記整理成一個好的文章也是一門學問!