流的重要性
流是Node.js最重要的组成和设计模式之一。社区流行这样一句花:“stream all the things”。可见stream在node.js的重要性,了解并学会它必须提上日程,在此记录下对stream的理解,如有错误欢迎指出。那我们开始深挖它吧!
流的分类
- stream.Readable:读取数据
- stream.Writable:写入数据
- stream.Duplex:可读+可写
- stream.Transform:在读写过程中,可对数据进行修改
可读流 Readable Stream
从可读流中获取数据有两种模式:非流动模式和流动模式
非流动模式
添加一个对于readable事件的监听器,在读取新的数据时进行通知。可以通过read()方法来实现,数据可以是String、Buffer、nul
readbale.read([size]);//读取指定size的数据
流动模式
是给data事件添加一个监听器。只要流中的数据可读,便会立即被推送到data事件的监听器。
如何从流中读取数据
let fs = require('fs');
let path = require('path');
//创建一个可读流。返回的是一个可读流对象
let rs = fs.createReadStream(path.join(__dirname,'./1.txt'),{
flag:'r',//文件的操作是读取操作
encoding:'utf8',//默认是null,null代表的是buffer
autoClose:true,//读取完毕后自动关闭
highWaterMark:3, //默认是64k 1024*6b
start:0, //开始读取位置
end:10 // 包前又包后,即读取11位
});
//默认情况下,不会将文件中的内容输出,内容会先创建一个buffer先读取3b的内容,放在那里不输出,非流动模式
re.setEncoding('utf8');//设置编码格式 和encoding:'utf8'功能一样
rs.on('open',function(data){
console.log('open');
})
rs.on('close',function(data){
console.log('close');
})
rs.on('error',function(err){
console.log('err');
})
//流动模式会疯狂触发data事件,直到数据读取完毕
rs.on('data',function(data){ //暂停模式 -> 流动模式
console.log(data);
})
rs.on('end',function(data){
console.log('end');
})
执行结果
open
1.txt的内容
end
close
可读流的暂停与恢复
rs.on('data',function(data){ //暂停模式 -> 流动模式
console.log(data);
rs.pause();//暂停方法 表示暂停读取,暂停data事件触发,并不会导致流转换回非流动模式(就像播放器的暂停)
})
setTimeout(function(){
re.resume();//过3秒恢复读取,恢复data事件的触发(就像播放器的播放按钮)
},3000)
可读流的实现
创建可读流实例
let fs = require('fs');
let EventEmitter = require('events');
class ReadStream extends EventEmitter{
constructor(path,options){ //两个参数:路径和对象
super();
this.path = path;
this.flags = options.flags || 'r';
this.autoClose = options.autoClose || true; //自动关闭
this.encoding = options.encoding || null;
this.highWaterMark = options.highWaterMark || 64*1024; //最高水位线
this.start = options.start || 0;
this.end = options.end;
this.len = 0; //当前存多少水 喝水是同步,倒水是异步,喝的仍然是当前杯子的水
this.buffer = Buffer.alloc(this.highWaterMark); //建立一个buffer每次读highWaterMark这么多
this.reading = false; //如果正在读取,就不会再读取
this.emittedReadable = false; //当缓存区长度为0时才会触发事件
this.pos = this.start;//读取的位置
this.open(); //打开文件,调用时还没拿到fd(fd:文件描述符)
//看是否监听了data事件,如果监听了,就要变成流动模式
this.flowing = null; //null就是暂停模式
this.on('newListener',(eventName,callback)=>{
if(eventName === 'data'){ //看是否监听了data事件
this.flowing = true;
this.read(); //开始读取 读highWaterMark的长度
}
})
}
}
module.exports = ReadStream;
我们来按照流的读取顺序来一一实现可读流
open方法
open(){ //打开文件
fs.open(this.path,this.flags,(err,fd)=>{
if(err){ //打开文件如果报错触发error事件
this.emit('error',err);
if(this.autoClose){ //是否自动关闭
this.destory(); //有问题就销毁掉
}
return;
}
this.fd = fd; //保存文件描述符
this.emit('open'); //文件打开了
})
}
read方法
//把读取的内容放在buffer里
read(){
if(typeof this.fd !== 'number'){ //此时文件还没打开
//当文件真正打开的时候,会出发open事件,触发事件后再执行read,此时fd肯定有了
return this.once('open',()=>this.read());
}
// 当获取到fd时 开始读取文件了
let howMuchToRead = this.end?Math.min(this.end-this.pos+1,this.highWaterMark): this.highWaterMark;
fs.read(this.fd,buffer,0,howMuchToRead,this.pos,(err,byteRead)=>{ //从哪开始读:0 ; 读取长度:howMuchToRead
if(byteRead>0){
this.pos += byteRead;
let data = this.encoding?this.buffer.slice(0,byteRead).toString(this.encoding):this.buffer.slice(0,byteRead);
this.emit('data',data);
if(this.pos>this.end){ //当读取的位置大于末尾,就是读取完毕了
this.emit('end');
this.destroy();
}
if(this.flowing){ //流动模式继续触发
this.read();
}
}else{
this.emit('end');
this.destroy();
}
})
}
destroy方法
destroy(){
//先判断有没有fd,有,关闭文件,触发close事件
if(typeof this.fd!== 'number'){
return this.emit('close'); //销毁
}
fs.close(this.fd,()=>{
this.emit('close');
})
}
pause和resume方法
pause(){
this.flowing = false;
}
resume(){
this.flowing = true;
this.read();
}
可写流 Writable Stream
可写流表示数据的目的地,使用流模块提供的抽象类Writeable实现。
let fs = require('fs');
let ws = fs.createWriteStream('./1.txt',{
flaga:'w',//读取的类型:写
mode:0o666,
autoClose:true,
highWaterMark:3, //默认写是16k
encoding:'utf8',
start:0
});
//flag表示的并不是是否写入,表示的是能否继续写,但是返回false内容不会丢失,还是会把内容放到内存中
let flag = ws.write('test','utf8',()=>{}) //参数:写入的内容(必须是字符串或者buffer)、编码格式、callback
console.log(flag);
ws.end(); //结束写入,调用此方法会把缓存区强制全部写入,并且关闭文件
ws.end('end'); //end传递参数,会调用write方法将内容写入完毕之后再关闭
//如果调用end方法之后就不会触发drain
ws.on('drain',function(){
console.log('drain');
})
//常见报错:write after end,即end之后就不用在调用write方法
可写流的实现
创建可写流实例
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends EventEmitter{
constructor(path,options){
super();
this.highWaterMark = options.highWaterMark || 16*1024;
this.mode = options.mode;
this.start = options.start || 0;
this.flags = options.flags || 'w';
this.encoding = options.encoding || 'utf8';
//可读流 要又一个缓存区,当正在写入文件时,内容要写入到缓存中
//在源码中是一个链表,我们这里把它改成数组来实现,做一个小改动 =>[]
this.buffer = [];
this.writing = false; //标识 是否正在写入
this.needDrain = false; //是否满足触发drain事件
this.pos = 0; //记录写入的位置
this.length = 0; //记录缓存区的大小
this.open();
}
}
module.exports = WriteStream;
open方法
不管读写文件,你首先得打开吧,要不读个xx?haha,开玩笑啦,言归正传,open方法实现
open(){
fs.open(this.path,this.flags,this.mode,()=>{
if(err){
this.emit('error',err);
if(this.autoClose){ //看是否自动关闭
this.destroy(); //销毁
}
return;
}
this.fd = fd;
this.emit('open');
})
}
write方法
可写流顾名思义,当然是write最重要喽,了解了write方法,万里长征就跨出了一大步,来吧骚年
write(chunk,encoding=this.encoding,callback){
chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk,encoding);//判读chunk是否是buffer,不是buffer转为buffer
//write 返回一个boolean类型
this.length+=chunk.length;
let ret = this.length<this.highWaterMark; //比较是否达到了缓存区的大小
this.needDrain = !ret; //是否需要触发needDrain
//判断是否正在写入,如果正在写入,就写入到缓存区中
if(this.writing){
this.buffers.push({
encoding,
chunk,
callback
})
}else{
//专门用来将内容写入到文件内
this.writing = true;
this._write(chunk,encoding,()=>this.clearBuffer());
}
return ret;
}
//清空缓存区
clearBuffer(){
let buffer = this.buffer.shift();
if(buffer){
this._write(buffer.chunk,buffer.encoding,()=>this.clearBuffer()); //不断回调清空buffer
}else{
if(this.needDrain){ //判断是否需要触发drain
this.emit('drain');
}
}
}
//这里才是真正的写^_^
_write(chunk,encoding,callback){
if(typeof this.fd!=='number'){ //先判断文件是否打开即fd的存在
return this.once('open',()=>this._write(chunk,encoding,callback));
}
fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,byteWritten)=>{
this.length -= byteWritten;
this.pos += byteWritten;
this.write = false;
callback();//清空缓存区内容
});
}
desroty方法
destroy(){
if(typeof this.fd!=='number'){
return this.emit('close');
}
fs.close(this.fd,()=>{
this.emit('close');
})
}
也许今天写的比较浅显,随着时间的推移也许会理解的更透彻,记录下今天,如果某天对流的概念理解更多,会持续修改补充此文章。2018-06-19