1. 流的概念
- 流是一組有序的,有起點和終點的位元組資料傳輸手段,而且有不錯的效率。
藉助事件和非阻塞I/O庫,流模組允許在其可用的時候動態處理,在其不需要的時候釋放掉。 - 流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。 stream 模組提供了基礎的 API 。使用這些 API 可以很容易地來構建實現流介面的物件。比如HTTP 伺服器request和response物件都是流。
- 流可以是可讀的、可寫的,或是可讀寫的。所有的流都是 EventEmitter 的例項
2. 為什麼使用流
如果讀取一個檔案,使用fs.readFileSync
同步讀取,程式會被阻塞,然後所有資料被寫到記憶體中。使用fs.readFile
讀取,程式不會阻塞,但是所有資料依舊會一次性全被寫到記憶體,然後再讓消費者去讀取。如果檔案很大,記憶體使用便會成為問題。
這種情況下流就比較有優勢。流相比一次性寫到記憶體中,它會先寫到到一個緩衝區,然後再由消費者去讀取,不用將整個檔案寫進記憶體,節省了記憶體空間。
- 不使用流時:會發現當檔案很大會導致記憶體佔用也非常大。
- 使用流時:
使用流會發現記憶體佔用會很小,流的過程還可以理解成如下圖:
3. 四種流型別
Node.js中有四種基本型別的流:
- Readable – 可讀操作。 (如 fs.createReadStream())
- Writable – 可寫操作。 (如 fs.createWriteStream())
- Duplex – 可讀可寫操作。(如 net.Socket)
- Transform – 在讀寫過程中可以修改和變換資料的 Duplex 流 (如 zlib.createDeflate())
4. 可讀流 createReadStream
createReadStream
實現了stream.Readable
介面的物件,將物件資料讀取為流資料,當監聽data事件後,開始發射資料
var util = require(`util`);
var fs = require("fs")
fs.createReadStream = function(path, options) {
return new ReadStream(path, options);
};
util.inherits(ReadStream, Readable);
複製程式碼
4.1 建立可讀流
var rs = fs.createReadStream(path,[options]);
1.path 讀取檔案的路徑
2.options
flags開啟檔案要做的操作,預設為`r`
encoding預設為null
start開始讀取的索引位置
end結束讀取的索引位置(包括結束位置)
highWaterMark讀取快取區預設的大小64kb
> 如果指定utf8編碼highWaterMark要大於3個位元組
複製程式碼
4.2 設定編碼
// 與指定{encoding:`utf8`}效果相同,設定編碼
rs.setEncoding(`utf8`);
複製程式碼
4.3 監聽data事件
// 一旦監聽data事件時,流就可以讀檔案的內容,並且發射data。
// 根據設定的讀取快取區預設大小來決定,讀一段發射一段,直到讀完。
// 預設情況下,監聽data事件後會不停的讀資料,然後出發data事件,觸發完data事件後,再次讀資料。不會停。
// 希望流有一個暫停和恢復觸發機制,見4.8 暫停和恢復觸發data
rs.on(`data`, function (data) {
console.log(data);
});
複製程式碼
4.4 監聽end事件
// 檔案讀完了,會觸發end事件
rs.on(`end`, function () {
console.log(`讀取完成`);
});
複製程式碼
4.5 監聽error事件
// 檔案讀取出錯了,會觸發error事件
rs.on(`error`, function () {
console.log("error");
});
複製程式碼
4.6 監聽open事件
// 如果是檔案流還會涉及到open和close兩個事件
rs.on(`open`, function () {
console.log("檔案開啟");
});
複製程式碼
4.7 監聽close事件
// 如果是檔案流還會涉及到open和close兩個事件
rs.on(`close`, function () {
console.log("檔案關閉");
});
複製程式碼
4.8 暫停和恢復觸發data
// 通過pause()方法和resume()方法
rs.on(`data`, function (data) {
console.log(data);
rs.pause(); // 暫停讀取和發射data事件
setTimeout(function () {
rs.resume(); // 恢復讀取並觸發data事件
},2000);
});
複製程式碼
以上可以看到:
open
在data
前,open
先開啟檔案,然後data
讀取完內容發射。end
在close
前,先發現檔案讀完了執行end
,然後再關閉檔案close
4.9 可讀流的兩種模式
-
可讀流事實上工作在下面兩種模式之一:
flowing
和paused
-
在
flowing
模式下, 可讀流自動從系統底層讀取資料,並通過EventEmitter
介面的事件儘快將資料提供給應用。 -
在
paused
模式下,必須顯式呼叫stream.read()
方法來從流中讀取資料片段。 -
所有初始工作模式為
paused
的Readable
流,可以通過下面三種途徑切換到flowing
模式:- 監聽
`data`
事件
var rs = fs.createReadStream(path,[options]); 複製程式碼
- 呼叫
stream.resume()
方法 - 呼叫
stream.pipe()
方法將資料傳送到Writable
- 監聽
-
可讀流可以通過下面途徑切換到
paused
模式:- 如果不存在管道目標(pipe destination),可以通過呼叫
stream.pause()
方法實現。 - 如果存在管道目標,可以通過取消
`data`
事件監聽,並呼叫stream.unpipe()
方法移除所有管道目標來實現。
- 如果不存在管道目標(pipe destination),可以通過呼叫
如果 Readable 切換到 flowing 模式,且沒有消費者處理流中的資料,這些資料將會丟失。 比如, 呼叫了 readable.resume() 方法卻沒有監聽 `data` 事件,或是取消了 `data` 事件監聽,就有可能出現這種情況。
5. 可寫流 createWriteStream
createWriteStream
實現了stream.Writable
介面的物件將流資料寫入到物件中
fs.createWriteStream = function(path, options) {
return new WriteStream(path, options);
};
util.inherits(WriteStream, Writable);
複製程式碼
5.1 建立可寫流
// 往可寫流裡寫資料時,不會立刻寫入檔案的,而會先寫入快取區,
快取區大小就是highWaterMark,預設16k。然後等快取區滿了之後再真正的寫入檔案裡。
var ws = fs.createWriteStream(path,[options]);
1. path寫入的檔案路徑
2. options
flags 開啟檔案要做的操作,預設為`w`
encoding 預設為utf8
start 開始位置
highWaterMark 寫入快取區的預設大小16kb
複製程式碼
5.2 write方法
// write方法有返回值flag,按理說返回false就不能往裡面寫了,但是真的寫了資料也不會丟失,會快取在記憶體裡。等快取區清空後再從記憶體讀取出來。
let flag = ws.write(chunk,[encoding],[callback]);
1. chunk寫入的資料buffer/string
2. encoding編碼格式chunk為字串時有用,可選
3. callback 寫入成功後的回撥
> 返回值為布林值,系統快取區滿時為false,未滿時為true(快取區不能接著寫返回false,能接著寫返回true)
複製程式碼
5.3 end方法
ws.end(chunk,[encoding],[callback]);
> 表明接下來沒有資料要被寫入 Writable 通過傳入可選的 chunk 和 encoding 引數,可以在關閉流之前再寫入一段資料 如果傳入了可選的 callback 函式,它將作為 `finish` 事件的回撥函式
複製程式碼
5.4 drain方法
- 當一個流不處在 drain 的狀態, 對 write() 的呼叫會快取資料塊, 並且返回 false。 一旦所有當前所有快取的資料塊都排空了(被作業系統接受來進行輸出), 那麼 `drain` 事件就會被觸發
- 建議, 一旦 write() 返回 false, 在 `drain` 事件觸發前, 不能寫入任何資料塊
// 監聽可寫流快取區清空事件
// 快取區滿了後被清空了才會觸發drain
ws.on(`drain`, function () {
console.log(`drain`);
});
複製程式碼
5.5 finish方法
- 在呼叫了 stream.end() 方法,且緩衝區資料都已經傳給底層系統之後, `finish` 事件將被觸發
ws.end(`結束`);
ws.on(`finish`, function () {
console.log("寫入完成");
});
複製程式碼
參考: