什麼是流
流在node中是非常重要的,gulp的task任務,檔案壓縮,和http中的請求和響應等功能的實現都是基於流來實現的。為什麼會有流的出現呢,因為我們一開始有檔案操作之後,包括寫檔案和讀檔案都會有一個問題,就是會把內容不停的讀到記憶體中,都讀取完之後再往外寫,這樣就會導致記憶體被大量佔用。為了解決這個問題流就誕生了,通過流,我們可以讀一點內容就往檔案中寫一點內容,並且可以控制讀取的速度。
流的種類有很多,最常用的有:
- ReadStream 可讀流
- WriteStream 可寫流
- 雙工流
- 轉換流
- 物件流(gulp)
流的特點:
- 有序的有方向的
- 流可以自己控制速率
什麼是讀和寫呢?
- 讀是將內容讀取到記憶體中
- 寫是將記憶體或者檔案的內容寫入到檔案內
流都是基於原生的fs操作檔案的方法來實現的,通過fs建立流。流是非同步方法,都有回撥函式,所有的 Stream 物件都是 EventEmitter 的例項。常用的事件有:
- open – 開啟檔案
- data – 當有資料可讀時觸發。
- error – 在接收和寫入過程中發生錯誤時觸發。
- close – 關閉檔案
- end – 沒有更多的資料可讀時觸發。
- drain – 當快取區也執行完了觸發
可讀流
let fs = require(`fs`);
let rs = fs.createReadStream(`./2.txt`, {
highWaterMark: 3,
flags:`r`,
autoClose:true,
start:0,
end:3,
encoding:`utf8`
});
複製程式碼
主要引數說明:
- highWaterMark 檔案一次讀多少位元組,預設是64×1024
- flags 型別,預設是r
- autoClose 預設是true ,讀取完畢後自動關閉
- start 讀取開始位置
- end 讀取結束位置,star和end都是包前包後的。
- endencoding 預設讀取的是buffer
一般讀取可以使用預設引數。預設建立一個流是非流動模式,預設不會讀取資料,
我們需要接收資料是基於事件的,我們要監聽一個data事件,資料會自動的流出來,資料從非流動模式變為流動模式。
讀取之前先把檔案開啟:
rs.on(`open`,function () {
console.log(`檔案開啟了`);
});
複製程式碼
內部會自動的觸發這個事件rs.emit(`data`),
不停的觸發data方法,直到資料讀完為止。
rs.on(`data`,function (data) {
console.log(data);
rs.pause(); // 暫停觸發on(`data`)事件,將流動模式又轉化成了非流動模式,可以用setTimeout(()=>{rs.resume()},5000)恢復
});
複製程式碼
檔案讀完後觸發end方法:
rs.on(`end`,function () {
console.log(`讀取完畢了`);
});
複製程式碼
最後關閉檔案:
rs.on(`close`,function () {
console.log(`關閉`)
});
複製程式碼
監控錯誤:
rs.on(`error`,function (err) {
console.log(err)
});
複製程式碼
可讀流實現原理解析
let fs = require(`fs`);
let ReadStream = require(`./ReadStream`);
let rs = new ReadStream(`./2.txt`, {
highWaterMark: 3,
flags:`r`,
autoClose:true,
start:0,
end:3,
encoding:`utf8`
});
複製程式碼
ReadStream.js
let fs = require(`fs`);
let EventEmitter = require(`events`);
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事件
}
})
}
pause(){
this.flowing = false;
}
resume(){
this.flowing =true;
this.read();
}
read(){ // 預設第一次呼叫read方法時還沒有獲取fd,所以不能直接讀
if(typeof this.fd !== `number`){
return this.once(`open`,() => this.read()); // 等待著觸發open事件後fd肯定拿到了,拿到以後再去執行read方法
}
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(); // 銷燬即可
}
});
}
// 開啟檔案用的
destroy() {
if (typeof this.fd != `number`) { return this.emit(`close`); }
fs.close(this.fd, () => {
// 如果檔案開啟過了 那就關閉檔案並且觸發close事件
this.emit(`close`);
});
}
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); // 觸發檔案的開啟的方法
});
}
}
module.exports = ReadStream;
複製程式碼
可寫流
可寫流有快取區的概念,
- 第一次寫入是真的向檔案裡寫,第二次再寫入的時候是放到了快取區裡
- 寫入時會返回一個boolean型別,返回為false時表示不要再寫入了,
- 當記憶體和正在寫入的內容消耗完後,會觸發一個事件 drain,
let fs = require(`fs`);
let ws = fs.createWriteStream(`2.txt`,{
flags: `w`,
highWaterMark: 3,
encoding: `utf8`,
start: 0,
autoClose: true,
mode: 0o666,
});
複製程式碼
引數說明:
- flags: 預設是w (寫)預設檔案不存在會建立,a 追加
- highWaterMark:設定當前快取區的大小
- encoding:檔案裡存放的都是二進位制
- start: 從哪開始寫
- autoClose: 預設為true,自動關閉(寫完之後銷燬)
- mode: 寫的模式,預設0o666,可讀可寫
let i = 9;
function write() {
let flag = true; // 表示是否能寫入
while (flag&&i>=0) { // 9 - 0
flag = ws.write(i--+``);
}
}
複製程式碼
drain只有嘴塞滿了吃完了才會觸發,不是消耗完就觸發
ws.on(`drain`,()=>{
console.log(`幹了`);
write();
})
write();
複製程式碼
可寫流實現原理
let fs = require(`fs`);
let WS = require(`./WriteStream`)
let ws = new WS(`./2.txt`, {
flags: `w`,
highWaterMark: 1,
encoding: `utf8`,
start: 0,
autoClose: true,
mode: 0o666,
});
let i = 9;
function write() {
let flag = true;
while (flag && i >= 0) {
i--;
flag = ws.write(`111`); // 987 // 654 // 321 // 0
console.log(flag)
}
}
write();
ws.on(`drain`, function () {
console.log(`幹了`);
write();
});
複製程式碼
WriteStream.js
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.open(); // fd 非同步的 觸發一個open事件當觸發open事件後fd肯定就存在了
// 寫檔案的時候 需要的引數有哪些
// 第一次寫入是真的往檔案裡寫
this.writing = false; // 預設第一次就不是正在寫入
// 快取我用簡單的陣列來模擬一下
this.cache = [];
// 維護一個變數 表示快取的長度
this.len = 0;
// 是否觸發drain事件
this.needDrain = false;
}
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`);
}
}
}
_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(); // 第一次就寫完了
})
}
write(chunk, encoding = this.encoding) { // 客戶呼叫的是write方法去寫入內容
// 要判斷 chunk必須是buffer或者字串 為了統一,如果傳遞的是字串也要轉成buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
this.len += chunk.length; // 維護快取的長度 3
let ret = this.len < this.highWaterMark;
if (!ret) {
this.needDrain = true; // 表示需要觸發drain事件
}
if (this.writing) { // 正在寫入應該放到記憶體中
this.cache.push({
chunk,
encoding,
});
} else { // 第一次
this.writing = true;
this._write(chunk, encoding, () => this.clearBuffer()); // 專門實現寫的方法
}
return ret; // 能不能繼續寫了,false表示下次的寫的時候就要佔用更多記憶體了
}
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);
});
}
}
module.exports = WriteStream;
複製程式碼
管道流
let fs = require(`fs`);
let rs = fs.createReadStream(`./2.txt`,{
highWaterMark:1
});
let ws = fs.createWriteStream(`./1.txt`,{
highWaterMark:3
});
rs.pipe(ws); // 會控制速率(防止淹沒可用記憶體)
複製程式碼
- pipe方法 叫管道,可以控制速率,pipe會監聽rs的on(`data`),將讀取到的內容呼叫ws.write方法
- 呼叫寫的方法會返回一個boolean型別
- 如果返回了false就呼叫rs.pause()暫停讀取
- 等待可寫流寫入完畢後 on(`drain`)在恢復讀取
pip實現原理
let RS = require(`./ReadStream`);
let WS = require(`./WriteStream`);
let rs = new RS(`./1.txt`,{
highWaterMark:4
})
let ws = new WS(`./2.txt`, {
highWaterMark: 1
});
rs.pipe(ws);
複製程式碼
ReadStream.js
let fs = require(`fs`);
let EventEmitter = require(`events`);
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;
this.end = options.end || null;
this.encoding = options.encoding || null;
this.flags = options.flags || `r`;
this.flowing = null;
this.buffer = Buffer.alloc(this.highWaterMark);
this.open();
// {newListener:[fn]}
this.on(`newListener`, (type) => { // 等待著 它監聽data事件
if (type === `data`) {
this.flowing = true;
this.read();// 開始讀取 客戶已經監聽了data事件
}
})
}
pause(){
this.flowing = false;
}
resume(){
this.flowing =true;
this.read();
}
read(){
if(typeof this.fd !== `number`){
return this.once(`open`,() => this.read());
}
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) => {
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();
}
});
}
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) {
if (this.autoClose) {
this.destroy();
}
this.emit(`error`, err);
return;
}
this.fd = fd;
this.emit(`open`, this.fd);
});
}
pipe(dest){
this.on(`data`,(data)=>{
let flag = dest.write(data);
if(!flag){
this.pause();
}
});
dest.on(`drain`,()=>{
console.log(`寫一下聽一下`)
this.resume();
});
}
}
module.exports = ReadStream;
複製程式碼