深入探究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 顺便学习下CSS选择器 奇偶匹配nth-child(even)
May 24 Javascript
jquery 查找新建元素代码
Jul 06 Javascript
js判断生效时间不得大于失效时间的思路及代码
Apr 23 Javascript
JQuery中如何传递参数如click(),change()等具体实现
Apr 28 Javascript
js 绑定键盘鼠标事件示例代码
Feb 12 Javascript
ExtJS4给Combobox设置列表中的默认值示例
May 02 Javascript
jQuery菜单插件用法实例
Jul 25 Javascript
JS获取url参数,JS发送json格式的POST请求方法
Mar 29 Javascript
用Vue写一个分页器的示例代码
Apr 22 Javascript
layui下拉列表select实现可输入查找的方法
Sep 28 Javascript
vue 解决数组赋值无法渲染在页面的问题
Oct 28 Javascript
深入理解Vue的数据响应式
May 15 Vue.js
微信小程序“摇一摇”的实例代码
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 adodb连接不同数据库
2009/03/19 PHP
php set_time_limit(0) 设置程序执行时间的函数
2010/05/26 PHP
PHP批量上传图片的具体实现方法介绍.
2014/02/26 PHP
Bootstrap+PHP实现多图上传功能实例详解
2018/04/08 PHP
PHP+MariaDB数据库操作基本技巧备忘总结
2018/05/21 PHP
PHP getName()函数讲解
2019/02/03 PHP
PHP count_chars()函数讲解
2019/02/14 PHP
JTrackBar水平拖动效果
2007/07/15 Javascript
认识延迟时间为0的setTimeout
2008/05/16 Javascript
javascript编码的几个方法详细介绍
2013/01/06 Javascript
JS获取下拉列表所选中的TEXT和Value的实现代码
2014/01/11 Javascript
nodejs命令行参数处理模块commander使用实例
2014/09/17 NodeJs
JavaScript中的call方法和apply方法使用对比
2015/08/12 Javascript
jQuery满意度星级评价插件特效代码分享
2015/08/19 Javascript
jquery获取复选框的值的简单实例
2016/05/26 Javascript
AngularJS入门教程之数据绑定原理详解
2016/11/02 Javascript
Vue.js实现模拟微信朋友圈开发demo
2017/04/20 Javascript
十分钟带你快速了解React16新特性
2017/11/10 Javascript
Vue 2.0学习笔记之使用$refs访问Vue中的DOM
2017/12/19 Javascript
vue-cli脚手架的安装教程图解
2018/09/02 Javascript
JavaScript显式数据类型转换详解
2019/03/18 Javascript
vue实现跳转接口push 转场动画示例
2019/11/01 Javascript
判断JavaScript中的两个变量是否相等的操作符
2019/12/21 Javascript
JS实现斐波那契数列的五种方式(小结)
2020/09/09 Javascript
jQuery实现移动端扭蛋机抽奖
2020/11/08 jQuery
[17:13]DOTA2 HEROS教学视频教你分分钟做大人-斯拉克
2014/06/13 DOTA
python 为什么说eval要慎用
2019/03/26 Python
Python中sys模块功能与用法实例详解
2020/02/26 Python
python GUI库图形界面开发之PyQt5菜单栏控件QMenuBar的详细使用方法与实例
2020/02/28 Python
Python PIL库图片灰化处理
2020/04/07 Python
雅高酒店中国:Accorhotels.com China
2018/03/26 全球购物
Carmen Sol官网:购买果冻鞋、手袋和配件
2021/01/01 全球购物
文明家庭先进事迹材
2014/01/27 职场文书
银行会计主管岗位职责
2014/10/01 职场文书
会议接待欢迎标语
2014/10/08 职场文书
公务员考察材料
2014/12/23 职场文书