深入探究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 选择器部分整理
Oct 28 Javascript
jQuery对html元素的取值与赋值实例详解
Dec 18 Javascript
js实现滚动条滚动到页面底部继续加载
Dec 19 Javascript
实现React单页应用的方法详解
Aug 02 Javascript
jQuery ajax MD5实现用户注册即时验证功能
Oct 11 Javascript
node.js入门学习之url模块
Feb 25 Javascript
基于express中路由规则及获取请求参数的方法
Mar 12 Javascript
15分钟深入了解JS继承分类、原理与用法
Jan 19 Javascript
Node.js从字符串生成文件流的实现方法
Aug 18 Javascript
Vue基于iview实现登录密码的显示与隐藏功能
Mar 06 Javascript
小程序实现可拖动的悬浮按钮
Sep 07 Javascript
Vue + ts实现轮播插件的示例
Nov 10 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制作的意见反馈表源码
2007/03/11 PHP
Linux下CoreSeek及PHP扩展模块的安装
2012/09/23 PHP
PHP CURL 多线程操作代码实例
2015/05/13 PHP
JavaScript Date对象使用总结
2009/05/14 Javascript
关于javascript DOM事件模型的两件事
2010/07/22 Javascript
jquery快捷动态绑定键盘事件的操作函数代码
2013/10/17 Javascript
jQuery 快速结束当前正在执行的动画
2013/11/20 Javascript
jQuery获得内容和属性方法及示例
2013/12/02 Javascript
Node.js事件驱动
2015/06/18 Javascript
纯javascript实现分页(两种方法)
2015/08/26 Javascript
一个php+js实时显示时间问题
2015/10/12 Javascript
js实现按钮控制带有停顿效果的图片滚动
2016/08/30 Javascript
利用jQuery插件imgAreaSelect实现图片上传裁剪(放大缩小)
2016/12/02 Javascript
使用base64对图片的二进制进行编码并用ajax进行显示
2017/01/03 Javascript
JS正则表达式判断有效数实例代码
2017/03/13 Javascript
Vue 表单控件绑定的实现示例
2017/08/11 Javascript
js装饰设计模式学习心得
2018/02/17 Javascript
JS实现的字符串数组去重功能小结
2019/06/17 Javascript
JavaScript修改注册表实例代码
2020/01/05 Javascript
在python中利用pycharm自定义代码块教程(三步搞定)
2020/04/15 Python
为什么说python适合写爬虫
2020/06/11 Python
举例详解HTML5中使用JSON格式提交表单
2015/06/16 HTML / CSS
HTML5 Canvas 实现圆形进度条并显示数字百分比效果示例
2017/08/18 HTML / CSS
英国豪华真皮和布艺沙发销售网站:Darlings of Chelsea
2018/01/05 全球购物
经济实惠的豪华背包和行李袋:Packs Project
2018/10/17 全球购物
MATCHESFASHION.COM美国官网:英国奢侈品零售商
2018/10/29 全球购物
面向中国市场的在线海淘美妆零售网站:Beauty House美丽屋
2021/03/02 全球购物
局域网定义和特性
2016/01/23 面试题
秘书专业自荐信范文
2013/12/26 职场文书
学校工作推荐信范文
2014/07/11 职场文书
乡镇党的群众路线教育实践活动领导班子对照检查材料
2014/09/25 职场文书
2015新年联欢晚会开场白
2014/12/14 职场文书
学校国庆节活动总结
2015/03/23 职场文书
2015婚礼主持词开场白
2015/05/28 职场文书
新学期主题班会
2015/08/17 职场文书
python实现学员管理系统(面向对象版)
2022/06/05 Python