流(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()
}
复制代码
结语
以上就是全部了,谢谢阅读!如有纰漏,多多指正。