精华 through2原理解析
发布于 6 年前 作者 zp1996 6734 次浏览 来自 分享

写在前面

through2经常被用于处理nodestream,假如使用过gulp的话,对于这个包一定不会陌生,如:

gulp.task('rewrite', () => {
  return gulp.src('./through/enter.txt')
    .pipe(through2.obj(function(chunk, enc, callback) {
      const { contents } = chunk;
      for (var i = 0; i < contents.length; i++) {
        if (contents[i] === 97) {
          contents[i] = 122;
        }
      }

      chunk.contents = contents;
      this.push(chunk);

      callback();
    }))
    .pipe(gulp.dest('./dist'));
});

这里将文件中所有的字符a转换为字符z,在写gulp插件时一定会应用到这个包,下面就来窥探一下这个使用率非常高的包。

Transform stream

through2的源码仅仅就100多行,本质上就是对于node原生的transform流进行的封装,先来看下Transform streamTransform是一个双工流,既可读,也可写,但是与Duplex还是有着一些区别,Duplex的写和读可以说是没有任何的关联,是两个缓冲区和管道互补干扰,而Transform将其输入和输出是存在相互关联的,中间做了处理。具体差别可以参考下面图片对比:

Duplex stream:

lALPBY0V4qIFROHNAVTNAjE_561_340.png_620x10000q90g.jpg

Transform stream:

3161163067-59eff6bf01468_articlex.jpeg

Transform stream的两个缓存区相互关联,对于每个缓冲区来说,highWaterMark为阈值,超过阈值后,将会停止读或者写操作,如:

let i = 0;
const readable = Readable({
  highWaterMark: 2,
  read: function () {
    var data = i < 26 ? String.fromCharCode(i++ + 97) : null;
    console.log('push', data);
    this.push(data);
  }
});

const transform = Transform({
  highWaterMark: 2,
  transform: function (buf, enc, next) {
    console.log('transform', buf.toString());
    next(null, buf);
  }
})

readable.pipe(transform);

stream流向为:

lALPBY0V4qIc0SnMzc0Bww_451_205.png_620x10000q90g.jpg

由于阈值为2,所以只能pushf,这时readable的缓存区已满,transform的读缓存区和写缓存区已经满了(由于transform的两个缓存区的阈值为2,所以写缓存区在写入b之后就已经满了,后续不能继续写入),全部满之后,自然停止了读取,最终e,f存在A中,c,d存在B中,a,b存在C中,想要解决很简单,在添加一个流向就可以:

readable.pipe(transform).pipe(process.stdout);

through2源码

在了解Transform stream之后,through2的源码非常的简单,就是对于其的一层封装,暴露出三个api(through2through2.objthrough2.ctor)而且三者接收的参数一致,因为都是由一个工厂方法创造出的:

function through2 (construct) {
  return function (options, transform, flush) {
    // 做了一些参数整理
    if (typeof options == 'function') {
      flush     = transform
      transform = options
      options   = {}
    }

    if (typeof transform != 'function')
      transform = noop

    if (typeof flush != 'function')
      flush = null

    return construct(options, transform, flush)
  }
}

来看一下through2对于Transform stream的再加工,也就是源码中的DestroyableTransform,与其名字一样,就是一个替我们实现好了destory方法的Transform stream

DestroyableTransform.prototype.destroy = function(err) {
  if (this._destroyed) return
  this._destroyed = true

  var self = this
  // 触发destory后,close掉流
  process.nextTick(function() {
    if (err)
      self.emit('error', err)
    self.emit('close')
  })
}

through2through2.obj全部是创造出一个再加工后的Transform,区别如下:

  • 后者开启了对象模式(objectMode属性为true),写入的参数不仅仅限制在string or uint8Array
  • 后者降低了阈值(highWaterMark16,而不是默认的16kb),这样做的原因,是为了和node的默认保持一致,具体可以参见这里

through2.ctor可以用来再次定制,其返回的是一个构造函数,用法可以参考下面:

const Tran = through.ctor(function(chunk, enc, callback) {
  console.log('transform', chunk.toString());
  callback(null, chunk);
});
const transform = new Tran();

写在最后

streamnode中有着非常广泛的应用,但是它使用起来却不是那么友好,throgh2的出现可以减少使用上的麻烦,其原理也非常的简单;以上内容均为本人理解,如有错误还请指出,不胜感激~

5 回复

through2确实是非常实用的模块,写的不错,貌似transform的图部分说的不太准确吧

@i5ting 感谢狼叔,狼叔能否详细的说下呢,我好更改下,别误导了大家~

@zp1996 transform强调的是转化,读不变,你看一下node的文档,里面说的非常清楚

正好看 flamegraph 源码用到这个包,写的不错,赞

来自酷炫的 CNodeMD

@i5ting 好的,谢谢狼叔啦,我去看下

回到顶部