前言
最近在学习nodejs,在学习到可读流的readable的时候产生了一些困惑。readable会在什么时间被触发,什么时间会开始继续进行数据读取,读取什么时候又会停止?
可读流中有两种读取数据流的事件
-
data
data读取可读流的数据相当于是将水龙头完全打开,让水以highWaterMark一次的速度哗啦啦的流,直到整个文件全部读完。中途可以关闭水龙头(readable.pause()),关闭之后也可重新开启(readable.resume())。
一个小栗子:- 首先在当前目录下创建一个1.txt文件,文件内容为:1234567890
- 然后创建一个js文件,以可读流中data事件的方式来读取1.txt。代码如下:
let fs = require('fs'); let path = require('path'); let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{ highWaterMark: 3 }); rs.on('data', function(data){ console.log(data); })上面的方式文件会一次读取三个(highWaterMark),一口气把文件全部的内容都读去完成,输出的结果如下:
<Buffer 31 32 33> <Buffer 34 35 36> <Buffer 37 38 39> <Buffer 30> -
readable
readable读取数据是可以随机的控制速率的,不限于highWaterMark。readable可以比喻为是从水池中取水,它有添水和取水时机的问题。
readable在两种情况下读取数据的情形是和data相同的,一种是rs.read()不指定size的时候,另一种是指定的size和highWaterMark相同的时候。之所以结果会相同,也与读取的规则有关。与data执行相同的栗子:
let fs = require('fs'); let path = require('path'); let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{ highWaterMark: 3 }); rs.on('readable', function(data){ console.log(rs.read()); })执行结果:
<Buffer 31 32 33> <Buffer 34 35 36> <Buffer 37 38 39> <Buffer 30> null最后为什么会有一个null呢?如果rs.read()指定了size,null又会在哪里输出呢? 我们带着这个疑问先往下看。
readable读取时机
-
什么时机会从文件读取数据到可读流内部缓存中?
- 开始执行时,可读流内部缓存中没有数据,会先读取highWaterMark(默认64kb,可指定)个字节的数据到内部缓存中。
- readable读取之后,如果内部缓存区中的字节少于highWaterMark个字节的时候,会主动再读取highWaterMark字节的数据到内部缓存中。
- 如果readable读取后,内部缓存中没有数据了,会主动再读取highWaterMark字节的数据到内部缓存中。这是2中的一种特殊情形。
- 当readable触发时,如果缓存中的数据不够读取,则会读取最邻近的2的高次幂个字节放入缓存中。例如本次readable要读取5个字节,但是缓存中字节小于5个,则会读取2^3个字节到缓存中。
-
什么时候会调用readable将内部缓存中的数据取出?
- 当可读流内部缓存区中字节数为0的时候,包括开始执行时的0和后期读取时被清空两种情形,会触发一次readable事件。
- 如果readable执行时,当前缓存区中的数据不够读取,则会在从文件读取数据到缓存(对应读取文件时机的第4条)后,再次触发readable事件。
- 当到达可读流底部的时候,也就是文件已经被全部读到缓存中,且调用过一次readable事件后,会再次触发readable事件。(这是一次校验清理缓存的过程,也是上面例子最后输出了null的原因)
-
几个简单的小栗子
例1:与data输出类似的第二种情况,指定rs.read的size为highWaterMark:let fs = require('fs'); let path = require('path'); let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{ highWaterMark: 3 }); rs.on('readable', function(data){ console.log(rs.read(3)); // 每次读取后内部缓存中剩余的字节数 console.log(rs._readableState.length) })输出结果:
<Buffer 31 32 33> 0 <Buffer 34 35 36> 0 <Buffer 37 38 39> 0 null 1 <Buffer 30> 0简单分析一下:
第一次输出<Buffer 31 32 33>之后缓存中字节被清空,会主动再去文件中读取数据(对应读取文件的第3种情形),读取完成后会触发readable事件(对应readable触发条件第1条);
因此会有第二次、及第三次的主动输出<Buffer 34 35 36>、<Buffer 37 38 39>;
在第三次输出后,会主动去读取文件内容,读出最后一个字节,这时再次去调用readable,发现缓存中的字节不够要读取的3个(对应读取文件的第4种情形),rs.read会返回null,同时会再去读取文件数据,然后再次触发readable(对应readable触发条件第2条),一次有了最后一次输出<Buffer 30>;例2:当读到流底部的时候会自动触发一次readable事件:
let fs = require('fs'); let path = require('path'); // let ReadStream = require('./chat'); let rs = fs.createReadStream(path.join(__dirname, '1.txt'),{ highWaterMark: 7 }); rs.on('readable', function(data){ // 每次读取前内部缓存中剩余的字节数 console.log(rs._readableState.length) console.log(rs.read(8)); // 每次读取后内部缓存中剩余的字节数 console.log(rs._readableState.length) })输出结果:
7 null 7 10 <Buffer 31 32 33 34 35 36 37 38> 2 2 <Buffer 39 30> 0结果分析:
程序开始执行的时候会先将highWaterMark个数据到内部缓存中(对应读取文件的第1种情形),读取完成之后触发readable(对应readable触发条件第1条);
readable读取时发现内部缓存中的数据,不够要读取的8个,这时候read方法返回null,同时去读取文件2^3个字节(对应读取文件的第4种情形);
读取之后缓存中的字节是10,文件中所有字节全部都读取出来了,再次调用readable(对应readable触发条件第2条),输出<Buffer 31 32 33 34 35 36 37 38>;
readable读取之后,已经到达了流的底部(对应readable触发条件第3条),会触发readable时间,将缓存中内容全部读出,输出<Buffer 39 30>,缓存长度清0
模拟实现readable
看了上面的例子我可能还是有点蒙圈,毕竟有些抽象。因此给大家用fs.read的方式简单的实现了一个readable。
let fs = require('fs');
let EventEmmitter = require('events');
function computeNewHighWaterMark(n) {
n--;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n++;
return n;
}
class ReadStream extends EventEmmitter {
constructor(path, options) {
super();
this.path = path;
this.flags = options.flags || 'r';
this.autoClose = options.autoClose || true;
this.encoding = options.encoding || 'utf8';
this.highWaterMark = options.highWaterMark || 64 * 1024;
this.start = options.start || 0;
// 当前缓存里内容的字节数
this.readableLength = 0;
// 缓存区
this.buffers = [];
// 当需要读取时,先判断是不是正在读取,如果正在读取时,就不要再去读取了
this.reading = false;
// 是否需要触发readable事件
this.emittedReadable = false;
// 文件当前要读取的位置
this.pos = this.start;
// 用于记录文件是否已经读到末尾
this.fileSize = fs.statSync(path).size;
// 读之前先打开文件,注意是异步的
this.open();
// 判断是否监听了readable事件,如果是则开始读取文件,第一次读取highWaterMark个字节
this.on('newListener', (type) => {
if (type === 'readable') {
this.read();
}
})
}
// 这里的read等同于rs.read([size]),n代表要读取的字节数
read(n) {
if(this.fileSize === this.pos && this.isLast){
// 如果上一次已经将文件读完了,则直接返回内部缓存区剩余的内容
return this.buffers.shift();
}
// 如果要读取的内容长度比缓存区中的长度大,则将highWaterMark置为最近的2的多少次方,重新触发read事件
if (n > this.readableLength) {
this.highWaterMark = computeNewHighWaterMark(n);
this.emittedReadable = true;
// 保证没有正在读取再进行读取
if (!this.reading) {
this.reading = true;
this._read();
}
}
// 如果定义了n,则本次要读取的内容存放在curReadBuf中进行返回,即curReadBuf是读取的结果
let curReadBuf;
// 如果内部缓存中有则进行取出,放入要返回的curReadBuf中
if (n > 0 && n <= this.readableLength) {
curReadBuf = Buffer.alloc(n);
let buf; // 用于存储每次循环从内部缓存中取出的数据
let index = 0; // curReadBuf中当前最大的索引
let flag = true; // 用于在内部for循环中实现跳出while循环
// 依次取内部缓存buffers中的数据,给curReadBuf进行赋值
// buffers中的状态是:[buffer<5,6,7,8>,buffer<9,10,11,12>]
while (flag && (buf = this.buffers.shift())) {
for (let i = 0; i < buf.length; i++) {
curReadBuf[index++] = buf[i];
if (index === n) {
// 如果数据已经取了n个,则跳出while循环和本次的for循环,同时更新内部缓存区的长度,并将取出的多余数据塞回内部缓存区
flag = false;
this.readableLength -= n;
// 将剩余没有消耗的在塞回到内部缓存区
let r = buf.slice(i + 1);
if (r.length) {
this.buffers.unshift(r);
}
break;
}
}
}
}
// 如果缓存区没有内容,需要先去读取内容,然后触发readable事件,这里只是标识可以触发readable事件,没有去读取
if (this.readableLength === 0) {
this.emittedReadable = true;
}
// 如果缓存区长度小于highWaterMark,且没有正在读取,则需要去读取数据
if ((this.readableLength < this.highWaterMark) && !this.reading) {
this.reading = true;
this._read();
}
// 如果本次读取的结果已经是文件底部了允许触发readable
if(this.fileSize === this.pos){
this.emittedReadable = true
}
return curReadBuf || null;
}
_read() {
// 如果文件还没有打开则等文件打开之后再进行读取
if (typeof this.fd !== 'number') {
return this.once('open', () => this._read());
}
// 每次读highWaterMark这么多
let buffer = Buffer.alloc(this.highWaterMark);
fs.read(this.fd, buffer, 0, buffer.length, this.pos, (err, byteRead) => {
// 读完了重置reading标识
this.reading = false;
// 如果有读出内容,则进行标识重置及将内容放入内置的缓存区(buffers)中
if (byteRead > 0) {
// 维护缓存的长度
this.readableLength += byteRead;
// 重置下次开始读文件的位置
this.pos += byteRead;
// 将读取到的buffer返给内置的缓存buffers中
this.buffers.push(buffer.slice(0, byteRead));
if (this.emittedReadable) {
//默认下一次不触发readable事件
this.emittedReadable = false;
// 可以读取了,默认缓存满了
this.emit('readable');
}
} else {
// 最后一次到达流底部的时候触发readable
this.isLast = true;
if (this.emittedReadable) {
this.emittedReadable = false;
this.emit('readable');
}
// 当读取不到内容时触发end事件
this.emit('end');
}
});
}
destroy() {
fs.close(this.fd, () => {
this.emit('close');
})
}
// 打开要读取的文件
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err);
if (this.autoClose) {
this.destroy();
}
return;
}
this.fd = fd;
this.emit('open', this.fd);
})
}
}
module.exports = ReadStream;
如何使用自己写的ReadStream呢?直接new个实例就好啦!还是来个栗子吧^_^
let path = require('path');
let ReadStream = require('./chat');
let rs = new ReadStream(path.join(__dirname, '1.txt'),{
highWaterMark: 7
});
rs.on('readable', function(data){
// 每次读取后内部缓存中剩余的字节数
console.log(rs.readableLength)
console.log(rs.read(8));
// 每次读取后内部缓存中剩余的字节数
console.log(rs.readableLength)
})