十--nodejs原理(stream)

一、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'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值