首先啥是流呢?
- 流是一組有序的,有起點和終點的位元組資料傳輸手段
- 它不關心檔案的整體內容,只關注是否從檔案中讀到了資料,以及讀到資料之後的處理
- 流是一個抽象介面,被 Node 中的很多物件所實現。比如HTTP 伺服器request和response物件都是流。
Node.js 中有四種基本的流型別
- Readable - 可讀的流 (例如 fs.createReadStream()).
- Writable - 可寫的流 (例如 fs.createWriteStream()).
- Duplex - 可讀寫的流 (例如 net.Socket).
- Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate()).
流中的資料有兩種模式,二進位制模式和物件模式
- 二進位制模式, 每個分塊都是buffer或者string物件.
- 物件模式, 流內部處理的是一系列普通物件.
所有使用 Node.js API 建立的流物件都只能操作 strings 和 Buffer物件。但是,通過一些第三方流的實現,你依然能夠處理其它型別的 JavaScript 值 (除了 null,它在流處理中有特殊意義)。 這些流被認為是工作在 “物件模式”(object mode)。 在建立流的例項時,可以通過 objectMode 選項使流的例項切換到物件模式。試圖將已經存在的流切換到物件模式是不安全的。
下面就簡單的介紹下 首先是可讀流
可讀流事實上工作在下面兩種模式之一:flowing 和 paused
- 在 flowing 模式下, 可讀流自動從系統底層讀取資料,並通過 EventEmitter 介面的事件儘快將資料提供給應用。
- 在 paused 模式下,必須顯式呼叫 stream.read() 方法來從流中讀取資料片段。
- 所有初始工作模式為 paused 的 Readable 流,可以通過下面三種途徑切換到 flowing 模式:
- 監聽 'data' 事件
- 呼叫 stream.resume() 方法
- 呼叫 stream.pipe() 方法將資料傳送到 Writable
- 可讀流可以通過下面途徑切換到 paused 模式:
- 如果不存在管道目標(pipe destination),可以通過呼叫 stream.pause() 方法實現。
- 如果存在管道目標,可以通過取消 'data' 事件監聽,並呼叫 stream.unpipe() 方法移除所有管道目標來實現。
如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 'data' 事件,或是取消了 'data' 事件監聽,就有可能出現這種情況。
可讀流的三種狀態
在任意時刻,任意可讀流應確切處於下面三種狀態之一:
-
readable._readableState.flowing = null
-
readable._readableState.flowing = false
-
readable._readableState.flowing = true
-
若 readable._readableState.flowing 為 null,由於不存在資料消費者,可讀流將不會產生資料。 在這個狀態下,監聽 'data' 事件,呼叫 readable.pipe() 方法,或者呼叫 readable.resume() 方法, readable._readableState.flowing 的值將會變為 true 。這時,隨著資料生成,可讀流開始頻繁觸發事件。
-
呼叫 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背壓”(back pressure), 將導致 readable._readableState.flowing 值變為 false。 這將暫停事件流,但 不會 暫停資料生成。 在這種情況下,為 'data' 事件設定監聽函式不會導致 readable._readableState.flowing 變為 true。
-
當 readable._readableState.flowing 值為 false 時, 資料可能堆積到流的內部快取中。
建立可讀流
let rs=fs.createReadStream(path.join(__dirname,'1.txt'),{
flags:'r',//檔案的操作 預設r
encoding:'utf8',//編碼 預設buffer
autoClose:true,//是否自動關閉 預設true
start:0,//開始讀取的位置 預設0
end:10,//結束讀取的位置(包含)預設讀完
highWaterMark:3,// 一次讀取多少位元組 預設讀是64k 也就是64*1024
//還有fd 檔案描述符 預設null ,mode 檔案操作許可權,預設為 0o666
})
//這裡的第一個引數路徑;要說下: 是相對於專案根目錄的;跟我們平常的相對路徑是不一樣的;
//返回值是一個讀流物件;
// 監聽data事件;獲取每一次讀到的內容;流切換到流動模式,資料會被儘可能快的讀出
rs.on('data',function (data) {
console.log(data);
});
//監聽end事件 該事件會在讀完資料後被觸發
rs.on('end', function () {
console.log('讀取完成');
});
//還有open close error一些事件 看名字就知道幹啥的
//暫停和恢復觸發data;通過pause()方法和resume()方法
rs.on('data', function (data) {
rs.pause();
console.log(data);
});
setTimeout(function () {
rs.resume();
},2000);
複製程式碼
可寫流
//和讀流引數基本差不多
let ws=fs.createWriteStream('1.txt', {
flags: 'w', //預設是'w'
encoding: 'utf8',//預設是utf8
highWaterMark: 2, // 寫入快取區的預設大小 預設是16k 16*1024
})
let flag=ws.write('aaaa','utf8',()=>{
console.log('寫入成功');
})
//引數:1.寫入的資料buffer/string 2.編碼編碼格式chunk為字串時有用可選, 3.寫入成功後的回撥
//返回值為布林值,系統快取區滿時為false,未滿時為true
//end方法;不是on('end',cb);在調完end方法後;不能再呼叫write方法
ws.end(chunk,[encoding],[callback]);
//表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料
ws.on('finish', () => {
console.error('所有的寫入已經完成!');
});
//在呼叫了 ws.end() 方法,且緩衝區資料都已經傳給底層系統之後, 'finish' 事件將被觸發。
//drain方法
//當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 'drain' 事件就會被觸發
//建議, 一旦 write() 返回 false, 在 'drain' 事件觸發前, 不能寫入任何資料塊
//感受下
let i = 10;
function write() {
let flag = true;
while (i && flag) {
flag = ws.write("1");
i--;
console.log(flag);
}
}
write(); //true false
ws.on('drain', () => {
console.log("drain");
write();
});
//true false drain 5次
複製程式碼
pipe 用法
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
複製程式碼
原理
var ws = fs.createWriteStream('./2.txt');
var rs = fs.createReadStream('./1.txt');
rs.on('data', function (data) {
var flag = ws.write(data);
if (!flag)
rs.pause();
});
ws.on('drain', function () {
rs.resume();
});
rs.on('end', function () {
ws.end();
});
//將資料的滯留量限制到一個可接受的水平,以使得不同速度的來源和目標不會淹沒可用記憶體。
複製程式碼
簡單的實現下可讀流
let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter {//可以on就是釋出訂閱
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || 'r';
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.end = options.end;
this.flowing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.buffer = Buffer.alloc(this.highWaterMark);
this.length = 0;
this.on('newListener', (type, listener) => {
if (type == 'data') {//檢測到有監聽data的事件 就變成流動模式
this.flowing = true;
this.read();
}
});
this.on('end', () => {
if (this.autoClose) {
this.destroy();
}
});
this.open();//只要呼叫這個類 預設開啟檔案
}
read() {
if (typeof this.fd != 'number') {
return this.once('open', () => this.read());
}
let n = this.end ? Math.min(this.end - this.pos, this.highWaterMark) : this.highWaterMark;
fs.read(this.fd,this.buffer,0,n,this.pos,(err,bytesRead)=>{
//引數: 檔案描述符 讀到哪裡(容器) 從哪開始 讀到哪個位置 偏移 回撥
if(err){
return;
}
if(bytesRead){
let data = this.buffer.slice(0,bytesRead);
data = this.encoding?data.toString(this.encoding):data;
this.emit('data',data);
this.pos += bytesRead;
if(this.end && this.pos > this.end){
return this.emit('end');
}
if(this.flowing)
this.read();
}else{
this.emit('end');
}
})
}
open() {
//開啟檔案 (非同步)
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit('error', err);
this.fd = fd;
this.emit('open', fd);
})
}
end() {
if (this.autoClose) {
this.destroy();
}
}
destroy() {
//關閉檔案
fs.close(this.fd, () => {
this.emit('close');
})
}
}
module.exports = ReadStream;
複製程式碼
簡單的實現下可寫流
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
constructor(path, options) {
super(path, options);
this.path = path;
this.fd = options.fd;
this.flags = options.flags || 'w';
this.mode = options.mode || 0o666;
this.encoding = options.encoding;
this.start = options.start || 0;
this.pos = this.start;
this.writing = false;
this.autoClose = true;
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.buffers = [];
this.length = 0;
this.open();
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) return this.emit('error', err);
this.fd = fd;
this.emit('open', fd);
})
}
write(chunk, encoding, cb) {
if (typeof encoding == 'function') {
cb = encoding;
encoding = null;
}
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding || 'utf8');
let len = chunk.length;
this.length += len;
let ret = this.length < this.highWaterMark;
if (this.writing) {
this.buffers.push({
chunk,
encoding,
cb,
});
} else {
this.writing = true;
this._write(chunk, encoding,this.clearBuffer.bind(this));
}
return ret;
}
_write(chunk, encoding, cb) {
if (typeof this.fd != 'number') {
return this.once('open', () => this._write(chunk, encoding, cb));
}
fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, written) => {
if (err) {
if (this.autoClose) {
this.destroy();
}
return this.emit('error', err);
}
this.length -= written;
this.pos += written;
cb && cb();
});
}
clearBuffer() {
let data = this.buffers.shift();
if (data) {
this._write(data.chunk, data.encoding, this.clearBuffer.bind(this))
} else {
this.writing = false;
this.emit('drain');
}
}
end() {
if (this.autoClose) {
this.emit('end');
this.destroy();
}
}
destroy() {
fs.close(this.fd, () => {
this.emit('close');
})
}
}
module.exports = WriteStream;
複製程式碼