深入探究node之Transform


Posted in Javascript onJuly 20, 2017

本文详细的介绍了node Transform ,分享给大家,希望此文章对各位有所帮助。

Transform流特性

在开发中直接接触Transform流的情况不是很多,往往是使用相对成熟的模块或者封装的API来完成流的处理,最为特殊的莫过于through2模块和gulp流操作。那么,Transform流到底有什么特点呢?

从名称上说,Transform意为处理,类似于生产流水线上的每一道工序,每道工序针对到来的产品作相应的处理;从结构上看,Transform是一个双工流,通俗的解释它既可以作为可读流,也可作为可写流。但是,node却对Transform流针对其特性做了更为特殊的定制,使Transform不是单纯的Duplex流。

Transform流由于包含了Readable和Writeable特性,因此Transform在实际使用中有着多种方式:它既可以只作为消费者消费数据,也可同时作为生产者和消费者完成数据中间处理。下面将逐渐深入内部阐述Transform的运行机理及使用技巧。

Transform内部架构

深入探究node之Transform

上图表示一个Transform实例的组成部分:Readable部分缓冲(数组)、内部_read函数、Writeable部分缓冲(链表)、内部_write函数、Transform实例必须实现的内部_transform函数以及系统提供的回调函数afterTransform。由于Transform实例同时拥有两部分缓冲,因此2个缓冲的存储、消耗的顺序也就需要了解,这对于后面使用原生Transform编写代码有很大的指导意义。

传统意义的流(即Readable和Writeable)的实现者都需要实现对应的内部函数_read()和_write(),对于Readable实例而言,_read函数用于准备从源文件中获取数据并添加到读缓冲中;对于Writeable实例_write函数则从写缓冲链表中一次刷入到磁盘中。它们分别对应了读写流程的首尾步骤,具体可以关注node中的Stream一文。

而Transform中的_read和_write函数的实现大有不同,由于需要兼顾流的处理,因此着重分析Transform的内部函数执行流程。

深入探究node之Transform

示例demo:

readable.pipe(transform);

以上段示例代码为例,transform作为消费者消费readable。

Transform的实例transform拥有transormState和readableState属性,保存了相关属性,如tranform状态信息、回调函数存储和编码等。transform作为消费者,会在其write函数中消费数据,在node中的Stream文中介绍了write函数的实现细节,通过内部调用_write函数实现数据的写入。而在Transform中_write函数已经重写:

1.保存transform收到的chunk数据、编码和函数(执行刷新写缓冲)

2.在一定条件下执行_read函数(当状态为非转换下,只要读缓冲大小未超过设定的大小,则执行_read)

如果一切顺利,readable的数据会顺利执行transform的**write->_write->_read**,那么原本负责填充读缓冲的_read在Transform中发生了哪些改变呢?

Transform.prototype._read = function(n) {
 var ts = this._transformState;

 if (ts.writechunk !== null && ts.writecb && !ts.transforming) {
  ts.transforming = true;
  this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform);
 } else {
  // mark that we need a transform, so that any data that comes in
  // will get processed, now that we've asked for it.
  ts.needTransform = true;
 }
};

可见,_read的实现非常简单,根据条件选择执行_transform函数。需要注意的是_read的参数n并未有使用,因为是否插入数据至读缓冲是由开发者在_transform中来决定。相信大家对_transform函数并不陌生,node规定Transform实例必须提供_transform函数,而该函数正是在_read中调用。

_transform有三个参数,第一个为待处理的chunk数据,第二个为编码,第三个为回调函数。前两个参数很好理解,我们可以在_transform中尽情的处理数据,最后调用回调函数完成处理。那么,这个回调函数究竟是什么? 它就是Transform架构图中的afterTransform函数,它有几个功能:

1.清空各种状态信息,如transformState对象的一些属性,用于下次处理数据使用

2.可选的保存处理结果至读缓冲区

3.刷新写缓冲区,执行下一阶段的数据流处理

可见,在afterTransform函数执行后,才基本宣告transform第一阶段的结束。为何是第一阶段呢?因为transform才完成了作为消费者(即Writeable)的作用,如果用户在_transform中传入了数据到写缓冲区,那么此时transform也同时是一个生产者,提供数据让后面的消费者消费数据,这就涉及到了Transform使用上的问题。

Transform的生产消费实例

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

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

readable.pipe(transform);

示例代码很简单,创建了一个可读流,向消费者提供a-z的小写字母;创建了一个转换流,在_transform函数中针对数据并不做处理仅作打点输出,并向回调函数传递数据至读缓冲区。我们的目的是通过transform输出26个小写字母,但是当前程序执行的结果并不让人满意:

执行结果:
push a
push b
transform a
push c
transform b
push d
push e
push f

tranform仅仅处理到字母b,readable也仅仅提供了a-f的数据便戛然而止,这是为何?

这一切都归结于transform对象。认真读过上文后我们知道,所有的Transform实例同时有两个缓冲区,其中写缓冲区用来接收生产者的数据进行转换操作,读缓冲区则缓存数据给消费者使用。而在当前的实现中,transform._transform函数输出了待处理数据,同时执行next(null, buf);。该函数上文已有分析,即afterTransform函数,第一个参数为Error实例,第二个则为存入读缓冲区的数据。在本例中,执行完_transform后将处理后的数据存入读缓冲区,等待后面的消费者消费读缓冲区的数据。可是,transform后面没有消费者了,因此transform在处理完字母b存入读缓冲区后,读缓冲区已经满了(设定highWaterMark为2,即读写缓冲区的最大值均为2字节)。当字母c、d也执行到tranform._write后,由于不满足执行transform._read的条件无法执行transform._transform函数,更无法执行afterTransform函数,导致无法刷新写缓冲区的数据,造成字母c、d贮存在写缓冲区。而字母e、f则由于transform的写缓冲区满(transform.write()返回false),只有存储在readable的读缓冲区中,等待消费。这就造成了死循环,readable和transform所有的缓冲区都满了,流也就停止了。

解决这个问题的方法很简单,有两种不同方案:

1.transform的读缓冲区保持为空

2.增加消费者消费transform的读缓冲区

其实本质上都是让transform的读缓冲区得到消耗。

第一种方案:

保证transform的读缓冲区为空:

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

只需向next函数传入null即可,这样transform消费完数据后即宣告数据处理结束,读缓冲区始终为空。

第二种方案:

添加消费者:

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

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

transform实现不变,只是添加了消费者process.stdout。这样也同时保证了transform的读缓冲区处于可添加状态,也给了afterTransform函数刷新写缓冲区的机会,开启新的数据处理流程。

through2的实现

through2的重头戏在于Transform流,使用through2的API可方便的创建一个Transform实例,完成数据流的处理。

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)
 }
}

module.exports = through2(function (options, transform, flush) {
 var t2 = new DestroyableTransform(options)

 t2._transform = transform

 if (flush)
  t2._flush = flush

 return t2
})

可见,through2模块仅仅是封装了Transform的构造函数,并封装了更为易用的objectMode模式。之所以建议使用through2创建Transform对象,不仅仅是因为其提供了方便的API,更主要的是为了兼容性。Transform对象是属于Stream2.0的特性,早先版本的node并没有实现,而通过through2创建的Transform实例在之前版本的node下仍可正常使用,这是由于through2并未引用node默认提供的stream模块,而是使用社区中较为流行的“readable-stream”模块。

总结

本文旨在深入through2中的使用的Transform流进行探究,并作为上一篇文章node中的stream的回顾和应用。通过文末简单的示例了解Transform在开发中可能出现的问题,学会随意切换Transform的生产者和消费者的身份,更好的指导实际开发。

以上所述是小编给大家介绍的node之Transform ,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
非常强大的 jQuery.AsyncBox 弹出对话框插件
Aug 29 Javascript
你需要知道的10个最佳javascript开发实践小结
Apr 15 Javascript
JavaScript 学习笔记之一jQuery写法图片等比缩放以及预加载
Jun 28 Javascript
你必须知道的Javascript知识点之&quot;this指针&quot;的应用
Apr 23 Javascript
JavaScript实现MIPS乘法模拟的方法
Apr 17 Javascript
浅谈JSON.stringify()和JOSN.parse()方法的不同
Aug 29 Javascript
详解ECharts使用心得总结
Dec 06 Javascript
javascript中mouseenter与mouseover的异同
Jun 06 Javascript
angularjs性能优化的方法
Sep 05 Javascript
简单了解JS打开url的方法
Feb 21 Javascript
autojs 蚂蚁森林能量自动拾取即给指定好友浇水的实现方法
May 03 Javascript
前端使用crypto.js进行加密的函数代码
Aug 16 Javascript
微信小程序“摇一摇”的实例代码
Jul 20 #Javascript
基于JavaScript实现微信抢红包功能
Jul 20 #Javascript
ReactNative短信验证码倒计时控件的实现代码
Jul 20 #Javascript
基于jQuery实现手风琴菜单、层级菜单、置顶菜单、无缝滚动效果
Jul 20 #jQuery
详解Angular CLI + Electron 开发环境搭建
Jul 20 #Javascript
JavaScript 基础表单验证示例(纯Js实现)
Jul 20 #Javascript
js 事件的传播机制(实例讲解)
Jul 20 #Javascript
You might like
PHP 使用pcntl和libevent 实现Timer功能
2013/10/27 PHP
php共享内存段示例分享
2014/01/20 PHP
php实现文章评论系统
2019/02/18 PHP
laravel 根据不同组织加载不同视图的实现
2019/10/14 PHP
两个select之间option的互相添加操作(jquery实现)
2009/11/12 Javascript
extjs 的权限问题 要求控制的对象是 菜单,按钮,URL
2010/03/09 Javascript
js 在定义的时候立即执行的函数表达式(function)写法
2013/01/16 Javascript
浅析js封装和作用域
2013/07/09 Javascript
input:checkbox多选框实现单选效果跟radio一样
2014/06/16 Javascript
判断是否存在子节点的实现代码
2016/05/18 Javascript
浅谈jquery中的each方法$.each、this.each、$.fn.each
2016/06/23 Javascript
JavaScript动态检验密码强度的实现方法
2016/11/09 Javascript
原生js实现返回顶部缓冲效果
2017/01/18 Javascript
JS奇技之利用scroll来监听resize详解
2017/06/15 Javascript
详解react-router如何实现按需加载
2017/06/15 Javascript
微信小程序 自定义消息提示框
2017/08/06 Javascript
浅谈JS中的反柯里化( uncurrying)
2017/08/17 Javascript
arcgis for js栅格图层叠加(Raster Layer)问题
2017/11/22 Javascript
除Console.log()外更多的Javascript调试命令
2018/01/24 Javascript
JavaScript设计模式之单例模式简单实例教程
2018/07/02 Javascript
JS/HTML5游戏常用算法之碰撞检测 包围盒检测算法详解【矩形情况】
2018/12/13 Javascript
JavaScript实现星级评价效果
2019/05/17 Javascript
在vue中把含有html标签转为html渲染页面的实例
2019/10/28 Javascript
利用Vue实现简易播放器的完整代码
2020/12/30 Vue.js
Python使用MYSQLDB实现从数据库中导出XML文件的方法
2015/05/11 Python
django-xadmin根据当前登录用户动态设置表单字段默认值方式
2020/03/13 Python
Python hashlib模块的使用示例
2020/10/09 Python
css3 iphone玻璃透明气泡完美实现
2013/03/20 HTML / CSS
几个判断型的面试题
2012/07/03 面试题
如何唤起类中的一个方法
2013/11/29 面试题
美术师范毕业生自荐信
2013/11/16 职场文书
局火灾防控工作方案
2014/05/25 职场文书
工作违纪检讨书范文
2015/01/26 职场文书
2015年团支部年度工作总结
2015/05/27 职场文书
MySQL中出现乱码问题的终极解决宝典
2021/05/26 MySQL
Windows下redis下载、redis安装及使用教程
2021/06/02 Redis