一、stream
shell通过管道|连接各部分,其输入输出的规范是文本流,在Node.js中,内置的Stream模块也实现了类似功能,各部分通过.pipe()连接。
Stream提供了以下四种类型的流:
var Stream = require('stream');
var Readable = Stream.Readable;
var Writable = Stream.Writable;
var Duplex = Stream.Duplex
var Transform = Stream.Transform;
用stream可实现数据的流式处理,如:
var fs = require('fs');
// `fs.createReadStream`创建一个`Readable`对象以读取`bigFile`的内容,并输出到标准输出
// 如果使用`fs.readFile`则可能由于文件过大而失败
fs.createReadStream(bigFile).pipe(process.stdout)
1、Readable–创建可读流
流式消耗迭代器中的数据
const Readable = require('stream').Readable;
class ToReadable extends Readable{
constructor(iterator){
super();
this.iterator = iterator;
}
//子类需要实现该方法
//这是生产数据的逻辑
_read(){
const res = this.iterator.next()
if(res.done){
//数据源已枯竭,调用‘push(null)’通知流
return this.push(null);
}
setTimeout(()=>{
//通过‘push’方法将数据添加到流里
this.push(res.value+'\n')
})
}
}
module.exports = ToReadable;
实际使用时,new ToReadable(iterator)会返回一个可读流,下游可以流式的消耗迭代器中的数据。
const iterator = function (limit){
return {
next:function(){
if(limit--){
return {done:false,value:limit+Math.random()};
}
return {done:true}
}
}
}(10)
const readable = new ToReadable(iterator);
//监听data事件,一次获取一个数据
readable.on('data',data=>process.stdout.write(data));//持续打印10个随机数
//所有数据均已读完
readable.on('end',()=>process.stdout.write('done'));//写入完成,就会触发end方法
(1)创建可读流时,需要继承Readable,并实现_read()方法
_read方法是从底层系统读取具体数据的逻辑,即生产数据的逻辑;
在_read方法中,通过调用push(data)将数据放入可读流中供下游消耗;
在_read方法中,通过同步调用push(data),也可以异步调用;
当全部数据都生产出来需要调用push(null)来结束可读流;
流一旦结束,便不能在调用push(data)添加数据了
(2)可以通过监听data事件的方式消耗可读流
在首次监听其data事件后,Readable便会持续不断的调用_read,通过触发data事件将数据输出。(一定要监听data事件,不然_read方法一次都不会执行)
第一次data事件会在下一个tick中触发,所以可以安全的将数据输出前的逻辑放在事件监听后(同一个tick中)
(3)当数据全部被消耗会触发end事件
2、Writable–创建可写流
可写流也是需要继承重写_write(data, enc, next)方法,有些简单的情况下不需要创建一类流,而只是一个流对象,可以用如下方式去做:
const Writable = require('stream').Writable
const writable = Writable();
//实现write方法
//这是将数据写入底层的逻辑
writable._write = function (data,enc,next){
//将流中的数据写入底层
process.stdout.write(data.toString().toUpperCase());//这里会依次打印a.b.c
//这里必须用toString是因为流是用buffer存储的(二进制),转为字符串可以让我们正常理解输出
//写入完成时,调用next()方法通知流传入下一个数据
process.nextTick(next);//这里就是异步输出,也可以直接next()
}
//所有数据均已写入底层
writable.on('finish',()=>process.stdout.write ('done'))
//将一个数据写入流中
writable.write('a'+'\n');
writable.write('b'+'\n');
writable.write('c'+'\n');
//再无数据写入流时,需要调用end方法
writable.end();
(1)上游通过调用writable.write(data)将数据写入可写流中。write方法会调用_write()将data写入底层
(2)在_write中,当数据成功写入底层后,必须调用next(err)告诉流开始处理下一个数据;
(3)next的调用既可以是同步的也可以是异步的
(4)上游必须调用writable.end(data)来结束可写流,data是可选的,此后不能在调用write新增数据;
(5)在end方法调用后,当所有的底层写操作均完成时,会触发finish事件。
3、Duplex–创建可读可写流
Duplex实际上就是继承了Readable和Writable的一类流。 所以,一个Duplex对象既可当成可读流来使用(需要实现_read方法),也可当成可写流来使用(需要实现_write方法)。
var Duplex = require('stream'.Duplex);
var duplex = Duplex()
//可读端底层读取逻辑
duplex._read = function(){
this._readNum = this._readNum||0;
if(this._readNum>1){
this.push(null);
}else{
this.push(''+(this._readNum++))
}
}
//可写端底层写逻辑
duplex._write = function (buf,enc,next){
process.stdout.write('_write'+buf.toString(+'\n'));//process.stdout是一个流
next();
}
duplex.on('data',data=>console.log('ondata',data.toString()));
duplex.write('a');
duplex.write('b');
duplex.write('x');
duplex.end();
//打印 a,b,x,0,1----写先打印出来,因为第一次data事件会在下一个tick中触发
上面代码实现了_read()方法,所以可以监听data事件来消耗duplex的数据,也实现了_wirte方法,可作为下游去消耗数据。
因为它既可读可写,所以称它为两端,可写端和可读端。可写端的接口与Writable一致,作为下游来使用;可读端的接口与Readable一致,作为上游来使用。
4、Transform–变换流
在上面的例子中,可读流中的数据(0, 1)与可写流中的数据(’a’, ‘b’)是隔离开的,但在Transform中可写端写入的数据经变换后会自动添加到可读端。 Tranform继承自Duplex,并已经实现了_read和_write方法,同时要求用户实现一个_transform方法。
const Transform = require('stream').Transform;
class Rotate extends Transform{
constructor(n){
super();
//将字母移动n个位置
this.offset = (n||13)%26
}
_transform(buf,enc,next){
var res = buf.toString().split('').map((c)=>{
var code = c.charCodeAt(0);
if(c>='a'&&c<='z'){
code += this.offset;
if(code>'z'.charCodeAt(0)){
code -= 26;
}
}else if(c>='A'&&c<='Z'){
code += this.offset;
if(code>'Z'.charCodeAt(0)){
code -+ 26;
}
}
return String.fromCharCode(code);
}).join('')
//调用push方法将变换后的数据添加到可读端
this.push(res);
//调用next方法准备处理下一个数据
next();
}
}
var transform = new Rotate(3)
transform.on('data',data=>process.stdout.write(data))
transform.write('hello');
transform.write('world')
transform.end();
5、数据类型
在shell中,用管道连接上下游。上游输出的是文本流,下游输入的也是文本流(标准输入输出流)。
对于可读流来说,push(data)时,data只能是string或者buffer类型,而消耗时,data事件输出的都是buffer类型。
对于可写流来说,write(data)时,data只能是string或buffer类型,_write(data)传进来的都是buffer类型。
也就是说流中默认的类型是buffer类型。产生的数据一放入流中,便转成buffer被消耗;写入的数据在传给底层写逻辑时也被转为buffer类型。
但每个构造函数中都接受一个配置对象,在其中设置选项ObjectMode为true,就能看到我们传入的是啥就是啥。
//不设置时:
const Readable = require('stream').Readable
const readable = Readable()
readable.push('a')
readable.push('b')
readable.push(null)
readable.on('data', data => console.log(data))//打印出来的是<buffer utf-8码值>
//设置时:
const Readable = require('stream').Readable
const readable = Readable({ objectMode: true })
readable.push('a')
readable.push('b')
readable.push({})
readable.push(null)
readable.on('data', data => console.log(data))//输出‘a’,'b'