今天的文章需要提前瞭解一下 node
中 fs
模組的相關 api
,不太熟悉的同學可以點這裡。
眾所周知,
node
中的fs
模組功能大都與檔案相關,比如可以通過fs.createReadStream
建立檔案可讀流,通過fs.createWriteStream
建立檔案可寫流,還可以通過監聽open
、data
、end
、error
、readable
事件對資料進行操作。由於時間有限,今天我們先來實現一下readable
事件功能。
開始之前,先簡單介紹一下可讀流函式 fs.createReadStream(path[, options])
中各引數所代表的含義,如下所示:
path <string> | <Buffer> | <URL>
建立可讀流的路徑options <string> | <Object>
可選引數flags <string>
檔案讀寫標識,預設為 rencoding <string>
讀取編碼格式,預設為 nullfd <integer>
檔案描述符,預設為 nullmode <integer>
檔案操作許可權,預設為 0o666autoClose <boolean>
檔案是否自動關閉,預設為 truestart <integer>
檔案讀取開始位置,預設為 0end <integer>
檔案讀取結束位置,預設為 InfinityhighWaterMark <integer>
水位線,每次讀取長度,預設為 64位元組(64 * 1024)
一、建立可讀流
首先我們需要實現一個可讀流的類,不防定義為 ReadableStream
,該類可以通過 on
函式進行事件監聽,所以需要繼承 node
中 EventEmitter
類;
當監聽 readable
函式時可讀取到檔案內容,由此得知在建構函式中除了需要定義上面的變數,還需要呼叫開啟檔案和第一次讀取檔案的功能。程式碼如下:
let fs = require('fs')
let EventEmitter = require('events')
class ReadableStream extends EventEmitter {
constructor(path, options) {
super()
this.path = path
this.flags = options.flags || 'r'
this.encoding = options.encoding || null
this.autoClose = options.autoClose || true
this.highWaterMark = options.highWaterMark || 64 * 1024
this.start = options.start || 0
this.end = options.end || null
this.mode = options.mode || 0o666
// 是否正在讀取檔案
this.reading = false
// 當len=0時,觸發readable事件
this.emitReadable = false
// 快取中位元組的長度
this.len = 0
// 快取每次讀取的內容,格式為[<Buffer />, <Buffer />, ...]
this.arr = []
// 檔案讀取的位置
this.pos = this.start
// 是否檔案全部讀取完
this.finished = false
// 開啟檔案
this.open()
// 判斷使用者是否監聽了readable事件
this.on('newListener', (type) => {
if (type === 'readable') {
// 第一次檔案讀取
this.read()
}
})
}
}
module.exports = ReadableStream
複製程式碼
建構函式中其它變數可以先忽略,到實現階段時我相信大家自然清晰其用處。
下面利用 fs.open
和 fs.destory
實現 open
和 destory
功能。實現程式碼如下:
// 開啟可讀流
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error')
if (this.autoClose) {
this.destory()
}
return
}
this.fd = fd
this.emit('open')
})
}
// 關閉可讀流,引數為檔案描述符
destory() {
if (typeof this.fd === 'number') {
fs.close(this.fd, () => {
this.emit('close')
})
}
this.emit('close')
}
複製程式碼
接下來看下初次讀取時的 read
函式。
實現思路:在建構函式中,當觸發第一次讀取檔案時,讀取大小為
highWaterMark
個,不防我們將比較讀取長度和快取長度的方法設定為read
。然後再read
函式中判斷,如果快取區長度為0
時,表明可以觸發readable
事件;如果快取區長度小於水位線時,則進行檔案讀取,此時我們將真正讀取檔案的函式命名為_read
;最後,根據編碼格式進行返回資料。
實現程式碼如下:
class ReadableStream extends EventEmitter {
// 此處如上,省略...
// 可讀流例項呼叫的方法
read () {
let buffer = null
// 如果快取區長度為0時,表明可以觸發readable事件
if (this.len === 0) {
this.emitReadable = true
}
// 如果快取區長度小於水位線時,則進行檔案讀取
if (this.len < this.highWaterMark) {
if (!this.reading) {
this.reading = true
this._read()
}
}
// 根據編碼方式處理資料
if (buffer) {
buffer = this.encoding ? buffer.toString(this.encoding) : buffer
}
return buffer
}
// 真實讀取檔案的方法
_read () {
// 因為開啟檔案為非同步操作,當讀取時檔案未開啟,可以通過註冊一次open事件,開啟後執行回撥即可拿到this.fd
if (typeof this.fd !== 'number') {
this.once('open', () => this._read())
return
}
let howMuchToRead = this.end ? Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark
let buffer = Buffer.alloc(howMuchToRead)
fs.read(this.fd, buffer, 0, this.howMuchToRead, this.pos, (err, bytesRead) => {
// bytesRead 為檔案讀取到的長度
if (bytesRead > 0) {
// 將讀取的內容快取到arr陣列中
this.arr.push(buffer)
// 相關變數更新
this.len += bytesRead
this.pos += bytesRead
this.reading = false
// 快取後觸發例項上使用者呼叫的read函式
if (this.emitReadable) {
this.emitReadable = false
this.emit('readable')
}
}
})
}
// 此處如上,省略...
}
複製程式碼
當前 1.txt
檔案中的內容為 1234567890
。呼叫方式如下:
let fs = require('fs')
let ReadableStream = require('./ReadableStream')
let rs = new ReadableStream('./1.txt', {
autoClose: true,
start: 0,
flags: 'r',
encoding: 'utf8',
highWaterMark: 3
})
rs.on('readable', () => {
})
複製程式碼
接下來從快取區中讀取資料。
二、讀取長度小於水位線
當讀取長度小於水位線時,使用原生方式呼叫,可以得到如下結果:
let fs = require('fs')
let rs = fs.createReadStream('./1.txt', {
autoClose: true,
start: 0,
flags: 'r',
encoding: 'utf8',
highWaterMark: 3
})
rs.on('readable', () => {
let r = rs.read(2)
// 輸出結果為 12
console.log(r)
})
複製程式碼
由此可知,如果緩衝區內容夠讀,則返回結果結束讀取。實現程式碼如下:
class ReadableStream extends EventEmitter {
// 此處如上,省略...
// 可讀流例項呼叫的方法
read (n) {
// 如果引數為空且不是在建構函式中呼叫此函式,n 預設按highWaterMark處理
if (typeof n === 'undefined' && this.pos > this.start) {
n = this.highWaterMark
}
// 如果讀取長度小於快取區長度,this.read(2) highWaterMark=3
if (n > 0 && n <= this.len) {
buffer = Buffer.alloc(n)
let current
let index = 0
let flag = true
while (flag && (current = this.arr.shift())) {
for (let i = 0; i < current.length; i++) {
buffer[index++] = current[i]
if (index === n) {
flag = false
let other = current.slice(i + 1)
if (other.length > 0) {
this.arr.unshift(other)
}
this.len -= n
break
}
}
}
}
// 如果快取區長度為0時,表明可以觸發readable事件
if (this.len === 0) {
this.emitReadable = true
}
// 如果快取區長度小於水位線時,則進行檔案讀取
if (this.len < this.highWaterMark) {
if (!this.reading) {
this.reading = true
this._read()
}
}
// 根據編碼方式處理資料
if (buffer) {
buffer = this.encoding ? buffer.toString(this.encoding) : buffer
}
return buffer
}
// 此處如上,省略...
}
複製程式碼
三、讀取長度等於水位線
當讀取長度等於水位線時,使用原生方式呼叫,可以得到如下結果:
let fs = require('fs')
let rs = fs.createReadStream('./1.txt', {
autoClose: true,
start: 0,
flags: 'r',
encoding: 'utf8',
highWaterMark: 2
})
rs.on('readable', () => {
let r = rs.read(2)
// 輸出結果為 12 34 56 78 90 null
console.log(r)
})
複製程式碼
由此可知,如果緩衝區內容讀完為空,則返回結果繼續讀取。實現程式碼如下:
class ReadableStream extends EventEmitter {
// 此處如上,省略...
read (n) {
// 此處如上,省略...
// 如果讀取長度等於水位線,this.len 等於 0,表明可以觸發readable事件,固_read後會觸發readable函式
if (this.len === 0) {
this.emitReadable = true
}
// 如果快取區長度小於水位線時,則進行檔案讀取
if (this.len < this.highWaterMark) {
if (!this.reading) {
this.reading = true
this._read()
}
}
// 此處如上,省略...
return buffer
}
// 此處如上,省略...
}
複製程式碼
四、讀取長度大於水位線
當讀取長度大於水位線時,使用原生方式呼叫,可以得到如下結果:
let fs = require('fs')
let rs = fs.createReadStream('./1.txt', {
autoClose: true,
start: 0,
flags: 'r',
encoding: 'utf8',
highWaterMark: 3
})
rs.on('readable', () => {
let r = rs.read(8)
// 輸出結果為 null 12345678 90
console.log(r)
})
複製程式碼
由此可知,如果緩衝區內容不夠讀,初次會返回 null
,然後修改 highWaterMark
值繼續讀取返回,即為 12345678
。此時,this.len
不等於 0
且小於 this.highWaterMark
,會再次呼叫 _read
方法,如果讀取檔案為空,則需要手動觸發一下 readable
事件。實現程式碼如下:
class ReadableStream extends EventEmitter {
// 此處如上,省略...
// 可讀流例項呼叫的方法
read (n) {
// 此處如上,省略...
// 如果this.read(8) highWaterMark=3
if (n > this.len) {
// 不夠讀時且檔案沒有讀取完,修改highWaterMark繼續讀取
if (!this.finished) {
this.highWaterMark = computeNewHighWaterMark(n)
this.reading = true
this.emitReadable = true
this._read()
} else {
// 否則直接返回快取資料
buffer = this.arr.shift()
}
}
// 此處如上,省略...
return buffer
}
// 真實讀取檔案的方法
_read () {
// 此處如上,省略...
fs.read(this.fd, buffer, 0, howMuchToRead, this.pos, (err, bytesRead) => {
// bytesRead 為檔案讀取到的長度
if (bytesRead > 0) {
// 此處如上,省略...
} else {
// this.len不等於0且小於this.highWaterMark,需要手動觸發一下readable事件
this.finished = true
if (this.len) {
this.emit('readable')
} else {
this.emit('end')
}
}
})
}
// 此處如上,省略...
}
複製程式碼
計算 highWaterMark
的函式如下:
function computeNewHighWaterMark (n) {
n--;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n++;
return n;
}
複製程式碼
???,好了,全部功能已經實現,就到此結束吧!