原文出自:https://www.pandashen.com
前言
本文是對於 NodeJS 核心模塊 fs
可讀流 createReadeStream
的應用,實現 “行讀取器”,功能爲讀取一個文檔的內容,每讀完一行觸發一次監聽的事件,並對這一行數據進行處理。
LineReader 類的創建
實現 “行讀取器” 的整體思路是創建一個類的實例,然後在這個實例上監聽一個事件,並開始讀取文件,每次讀完一行觸發,我們這裏將這個類命名爲 LineReader
,因爲類需要監聽事件,所以需要繼承 EventEmitter
。
// 行讀取器 LineReader 類
// 引入依賴
const EventEmitter = require("events");
const fs = require("fs");
// 行讀取器的類,參數爲讀取文件的路徑
class LineReader extends EventEmitter {
contructor(path) {
super();
this.path = path; // 文件路徑
this._rs = fs.createReadStream(this.path); // 創建可讀流
this.current = null; // 存儲每次讀到的單個字節
this.arr = []; // 存放文件每一行單個字節 Buffer 的數組
this.system = null; // 默認的系統(windows 或 mac)
this.RETURN = 13; // \r 的十六進制數
this.Line = 10; // \n 的十六進制數
// 監聽 newListener
this.on("newListener", readLineCallback.bind(this));
}
}
在 LineReader
實例上定義了 system
(當前系統)、current
(每次讀取的單個字節)、RETURN
(\r
十六進制編碼)和 Line
(\n
十六進制編碼)等屬性方便後面使用。
我們希望在監聽的事件觸發之前,就執行讀取文件一行內容的邏輯,就說明我們需要一個在監聽事件時就能執行的函數,那就需要在創建實例之前先監聽 newListener
事件,把 newListener
的回調來作爲這個函數執行,並能順帶在參數中獲取事件類型。
我們把讀取文件的核心邏輯放在了 newListener
事件的回調函數中,將這個回調函數命名爲 readLineCallback
,爲了保證執行時 readLineCallback
內部使用的 this
是 LineReader
的實例,使用 bind
進行修正。
行讀取器核心邏輯 readLineCall 函數
如果需要默認就開始讀取,並且每次讀取一個字節後還可以進行下一次循環讀取,這種場景最符合可讀流的暫停模式 readable
事件默認觸發一次,“容器” 內讀走了一個字節,就會自動 “續杯” 的特點。
// 行讀取器的核心邏輯
function readLineCallback(type) {
// 使用暫停模式進行讀取
this.on("readable", () => {
if (type === "newLine") {
// 爲了與 \r 和 \n 對比,每次只讀一個字節
while ((this.current = this._rs.read(1))) {
// 結果爲 Buffer,所以使用索引取出對比
switch (this.current[0]) {
case RETURN: // 針對 Windows
this.system = "windows";
this.disposeLine(); // 處理換行邏輯
break;
case LINE: // 針對 Mac
this.system = "mac";
this.disposeLine(); // 處理換行邏輯
break;
default:
// 每讀到換行的字符存入數組中
this.arr.push(current);
}
}
}
});
// 防止最後一行丟失
this.on("end", this.disposeLine.bind(this));
}
在上面代碼中監聽了 readable
事件並驗證了事件類型是否爲 newLine
,然後循環讀取文件內容,爲了與換行的十六進制碼進行對比,每次只讀取一個字節,當遇到換行符時,明確當前系統並調用換行符處理函數 disposeLine
進行處理。
注意:在最後一次的時候文件最後一行可能沒有換行,所以不滿足 switch
內語句的條件,即沒使用 disposeLine
進行處理,所以監聽可讀流的 end
事件,並在 end
觸發時讓 disposeLine
作爲回調函數執行,注意使用 bind
修正 this
爲當前實例。
兼容 Windows 和 Mac 的換行符處理函數
在換行符處理函數中,Windows 與其他系統(Mac、Linux)系統唯一的區別就是 Window 系統的換行符爲 \r\n
,比 Mac 和 Linux 的 \n
多了一個字節,而在讀取下一行時,這個字節是無用的,需要忽略。
// 換行符處理函數
LineReader.prototype.disposeLine = function() {
// 將這一行的內容發射出來並清空數組
this.emit("newLine", Buffer.concat(this.arr).toString());
this.arr = [];
// 如果是 window 系統,下一個是 \n,就往下多讀一個字節不存入組即可
if (this.system === "windows") {
this._rs.read(1);
}
};
驗證 LineReader 行讀取器
創建一個 “行讀取器” 需要創建 LineReader
類的實例,並傳入被讀取文件的路徑,由於在源碼中執行的是 newListener
的回調函數,所以只需添加 newLine
事件監聽就可以了,然後會在 readable
默認觸發時在內部循環讀取,並把每行讀到的內容重新整合後發送,實現 newLine
事件的連續觸發,直到文件讀完。
// 使用行讀取器
// 創建文件 1.txt 每次內容爲 1~9 9個數字,每 3 個字符爲一行
let lineReader = new LineReader("1.txt");
lineReader.on("newLine", data => {
console.log(`------ ${data} ------`);
});
// ------ 123 ------
// ------ 456 ------
// ------ 789 ------
“行讀取器” lineReader
對讀取到每一行的數據進行處理的邏輯主要在 newLine
事件的回調函數中,比如上面例子,在每一行的前、後添加了 ------
並打印。
總結
在 NodeJS 中,流的應用非常廣泛,“行讀取器” 只是其中的一種表現,可以根據流的不同模式的不同特性實現更復雜的功能,所以流在 NodeJS 中還是非常重要的。