流(Stream)到底是什麼
流(Stream)是資料的集合,就跟陣列和字串一樣。不同點就在於Streams可能不是立刻就全部可用,並且不會全部載入記憶體。這使得他非常適合處理大量資料,或者處理每隔一段時間有一個資料片段傳入的情況。
Node.js 提供了多種流物件。 例如, HTTP
請求 和 process.stdout
就都是流的例項。
流可以是可讀的、可寫的,或是可讀寫的。所有的流都是 EventEmitter
的例項。
流的型別
Node.js 中有四種基本的流型別:
- Readable - 可讀流 (例如 fs.createReadStream())
- Writable - 可寫流 (例如 fs.createWriteStream())
- Duplex - 可讀寫的流(雙弓流) (例如 net.Socket)
- Transform - 在讀寫過程中可以修改和變換資料的 Duplex 流 (例如 zlib.createDeflate())
本次介紹可讀流及其原始碼實現
fs.createReadStream()可讀流介紹
可讀流的建立
let fs = require('fs')
let rs = fs.createReadStream('./1.txt', {
highWaterMark: 3, // 位元組
flags:'r',
autoClose:true, // 預設讀取完畢後自動關閉
start:0,
end:3,// 流是閉合區間 包start也包end
encoding:'utf8'
})
複製程式碼
highWaterMark
每次讀取的位元組數,預設為64kbautoClose
讀取結束是否關閉檔案start
和end
讀取檔案結束(包含)和開始(包含)的位置,encoding
讀取流之後的編碼
通過fs.createReadStream()就可以建立一個可讀流。
可讀流的事件
可讀流有如下事件
rs.on('open', function () {
console.log('檔案開啟了')
})
rs.on('data', function (data) {
console.log('輸出資料', data.toString())
})
rs.on('end', function () {
console.log('讀取完畢')
})
rs.on('close', function () {
console.log('檔案關閉了')
})
rs.on('error', function (err) {
console.log('出錯了', err)
})
複製程式碼
open
檔案開啟時,會被觸發data
讀取流的資料時觸發end
流讀取結束觸發close
檔案關閉時觸發error
流讀取失敗觸發
可讀流的方法
可讀流有兩個很重要的模式(flawing)影響了我們使用的方式。
- 暫停模式
- 流動模式
所有的可讀流開始的時候都是預設暫停模式,但是它們可以輕易的被切換成流動模式,當我們需要的時候又可以切換成暫停模式。有時候這個切換是自動的。
可以使用resume()
和pause()
方法在這兩種模式之間切換。
當一個流是流動模式的時候,資料是持續的流動,我們需要使用事件去監聽資料的變化。
在流動模式中,如果可讀流沒有監聽者,可讀流的資料會丟失。這就是為什麼當可讀流逝流動模式的時候,我們必須使用data
事件去監聽資料的變化。事實上,只需新增一個data
事件處理程式即可將暫停的流轉換為流模式。
可讀流的實現
可以根據上面介紹的可讀流特性,實現一個可讀流的類
引入Node.js 模組
顯然可讀流是需要 fs
、events
這兩個模組的
let fs = require('fs')
let EventEmit = require('events')
複製程式碼
構造方法
class ReadStream extends EventEmit {
constructor(path, options = {}) {
super()
this.path = path
this.highWaterMark = options.highWaterMark || 64 * 1024
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end || null
this.encoding = options.encoding || null
this.flags = options.flags || 'r'
// 引數處理
this.flawing = false // 預設暫停模式
}
}
module.exports = ReadStream
複製程式碼
ReadStream
類是繼承events
模組的,預設是暫停模式(this.flawing = false
),其他是一些引數的處理。
讀取資料之前的一些處理
讀資料之前,需要開啟檔案,open方法實現
class ReadStream extends EventEmit {
constructor(path, options = {}) {
...
// 引數處理
this.flawing = false
// 開啟檔案 非同步
this.open()
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
this.destroy()
return
}
this.fd = fd
this.emit('open')
})
}
}
複製程式碼
需要用到fs.open(),這個方法是非同步的。開啟檔案失敗則發射error
事件,並銷燬(destroy
)可讀流的例項。成功則儲存檔案描述符(fd
),發射open
事件
destroy()
方法實現
destroy() {
if (typeof this.fd !== 'number') {
// 檔案沒有開啟
return this.emit('close')
}
fs.close(this.fd, () => {
this.emit('close')
})
}
複製程式碼
分兩種情況
- 檔案開啟失敗,直接發射
close
事件 - 流讀取結束需要關閉,使用
fs.close()
關閉檔案,回撥中觸發close
事件
開始讀取資料
read()
方法之後(這是非同步的,檔案描述符並沒拿到)就需要讀取資料了。流建立時,預設是暫停模式,只有新增了data
事件,才會轉換為流動模式。
構造方法中新增:
//同步執行
class ReadStream extends EventEmit {
constructor(path, options = {}) {
...
// 開啟檔案 非同步
this.open()
//同步執行
this.on('newListener', (type) => {
if (type === 'data') {
this.flawing = true
this.read()
}
})
}
}
複製程式碼
當例項上新增有data
事件,就呼叫read()
方法讀取資料。事件監聽這裡是同步的,通俗的說: read()
要比open
先執行。明白這點很關鍵,後面的read要處理fd沒有拿到。
read方法實現:
read() {
// 檔案沒有開啟,可能就開始讀取
if(typeof this.fd !== 'number') {
return this.once('open', this.read)
}
let howMuchToRead = this.end ?Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark
fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (err, bytesRead) => {
// 讀取完畢,位置向後移動
this.pos += bytesRead
// 發射資料
let result = this.buffer.slice(0, bytesRead)
let encodedResult = this.encoding ? result.toString(this.encoding) : result
this.emit('data', encodedResult);
// 如果還沒結束,繼續讀
if(bytesRead === this.highWaterMark && this.flawing) {
this.read()
}
// 沒有讀滿,說明結束了
if(bytesRead < this.highWaterMark) {
this.emit('end')
this.destroy()
}
})
}
複製程式碼
1、可讀流新增data
事件時,會成流動模式,開始執行read()
方法讀取資料,此時檔案並沒有開啟,因此需要open
事件觸發後,執行read
方法。
2、fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, callback)方法介紹
this.fd
開啟檔案拿到的檔案描述符this.buffer
讀取檔案buffer存放。建構函式中初始化this.buffer = Buffer.alloc(this.highWaterMark)
。0
存放到this.buffer
中的偏移量howMuchToRead
每次從檔案中讀取的長度this.pos
每次從檔案中讀取的位置,需要自己累加維護callback
讀取之後的回撥,其中bytesRead
是buffer的長度
要點提醒:
1、每次讀取長度howMuchToRead
的計算;
2、發射資料時需要從this.buffer
中擷取bytesRead
位數;
3、暫停模式下不能讀取(this.flawing=== false)
4、剩下的就是正常流程:this.pos
維護累加、沒讀完繼續讀取、讀完之後發射end
事件,並銷燬。
pause()和resume()的事件
直接上程式碼了,一看就明白
pause() {
this.flawing = false
}
resume() {
this.flawing = true
this.read()
}
複製程式碼
結語
以上就是全部了,謝謝閱讀!如有紕漏,多多指正。