广告
返回顶部
首页 > 资讯 > 前端开发 > node.js >深入探究node之Transform
  • 545
分享到

深入探究node之Transform

nodeTransform 2022-06-04 17:06:54 545人浏览 独家记忆
摘要

本文详细的介绍了node TransfORM ,分享给大家,希望此文章对各位有所帮助。 Transform流特性 在开发中直接接触Transform流的情况不是很多,往往是使用相对成熟的模块或者封装的api

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

Transform流特性

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

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

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

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的内部函数执行流程。

查看图片

示例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 ,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对编程网网站的支持!

--结束END--

本文标题: 深入探究node之Transform

本文链接: https://www.lsjlt.com/news/12492.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入探究node之Transform
    本文详细的介绍了node Transform ,分享给大家,希望此文章对各位有所帮助。 Transform流特性 在开发中直接接触Transform流的情况不是很多,往往是使用相对成熟的模块或者封装的API...
    99+
    2022-06-04
    node Transform
  • Electron架构深入探究
    目录Electron是什么Electron架构小结Electron是什么 引用来自官网的解释: Electron 是一个使用 JavaScript、 HTML 和 CSS 构建桌面...
    99+
    2023-02-09
    Electron架构 Electron架构探究
  • AndroidHandler源码深入探究
    1.android 消息循环有4个重要的类Handler、Message、Looper、MessageQueue handler 用来发送、处理消息。 Message 是消息的载体。...
    99+
    2022-11-13
  • Golang中map的深入探究
    目录简介Map 的底层内存模型Map 的存与取底层代码Map 的扩容第一种情况第二种情况Map 的有序性Map 的并发总结简介 本文主要通过探究在golang 中map的数据结构及源...
    99+
    2022-11-11
  • VueComputed底层原理深入探究
    今天面了家小公司,上来直接问 computed 底层原理,面试官是这样问的,data 中定义了 a 和 b 变量。computed 里面定义了 c 属性,c 的结果依赖与 a 和 b...
    99+
    2022-11-13
  • iOS坐标系的深入探究
    前言 app在渲染视图时,需要在坐标系中指定绘制区域。 这个概念看似乎简单,事实并非如此。 When an app draws something in iOS, it has...
    99+
    2022-05-15
    ios 坐标系 转换
  • Android 中的注解深入探究
    本文系GDG Android Meetup分享内容总结文章 注解是我们经常接触的技术,Java有注解,Android也有注解,本文将试图介绍Android中的注解,以及Butt...
    99+
    2022-06-06
    注解 Android
  • C++深入探究友元使用
    目录友元特点外部函数友元成员函数友元总结类友元友元 友元 friend 机制允许一个类授权其他的函数访问它的非公有成员. 友元声明以关键字 friend 开头 ,它只能出现在类的声明...
    99+
    2022-11-13
  • Java synchronized与死锁深入探究
    目录1.synchronized的特性2.synchronized使用示例:3.Java标准库中的线程安全类4.死锁是什么5.如果避免死锁1.synchronized的特性 1). ...
    99+
    2023-01-30
    Java synchronized Java 死锁
  • C语言深入探究动态规划之区间DP
    目录写在前面石子合并写在前面 之前讲过背包问题,线性DP不知道大家忘了吗,这次是区间DP 石子合并 题意: 合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价 解题思路: ...
    99+
    2022-11-13
  • C语言深入探究动态规划之线性DP
    目录写在前面数字三角形最长上升子序列最长上升子序列 II最长公共子序列写在前面 之前讲过背包问题,不知道大家忘了吗,如果忘了可以点这里,这次是线性DP 数字三角形 状态表示:f[i...
    99+
    2022-11-13
  • Java深入探究Object类的方法
    目录1.equals方法1.API中equals方法的介绍2.==和equals 的对比2.hashCode方法3.toString方法4.finalize方法本文主要带大家看看Ob...
    99+
    2022-11-13
  • C/C++多态深入探究原理
    目录多态虚表和虚表指针多态 面向对象编程有三大特性:继承、封装和多态。 其中,多态又分为编译时多态和运行时多态。编译多态是通过重载函数体现的,运行多态是通过虚函数体现的。 多态是如何...
    99+
    2022-11-13
  • C/C++函数指针深入探究
    目录函数指针语法函数地址声明使用函数指针调用函数深入理解函数指针使用typedef 简化函数指针 为什么要使用函数指针? 调用的灵活性和通用性。 试想一下,我们在设计初期并不知道我们...
    99+
    2022-11-13
  • C++深入探究引用的使用
    目录一. 引用的概念二. 引用特性三. 常引用四. 使用场景1. 做参数2. 做返回值3. 做返回值需要注意的问题五. 传值传引用效率对比1. 值和引用传参时的效率比较2. 值和引用...
    99+
    2022-11-13
  • MySQL中join查询的深入探究
    目录前引索引对 join 查询的影响数据准备有索引查询过程无索引查询过程了解 Block Nested-Loop JoinBlock Nested-Loop Join查询过程Join...
    99+
    2022-11-13
    mysql join查询 流程 mysql join方式 mysql join查询
  • Vue中v-bind原理深入探究
    目录前置内容解析模板总结前面我们分析了v-model的原理,接下来我们看看v-bind的实现又是怎样的呢? 前置内容 <template> <div> ...
    99+
    2022-11-13
    Vue v-bind Vue v-bind原理 Vue v-bind作用
  • SQL深入探究存储的过程
    目录存储过程简介存储过程的创建及调用存储过程的删除存储过程的优缺点现需要向学生表中插入新的学生数据。但在插入学生数据的时,需要同 时检查老师表里的数据。如果插入学生的老师不在老师表里,则先向老师表中插入一条老师数据,再向...
    99+
    2023-01-05
    SQL存储过程的作用 SQL存储过程
  • SpringCloud@RefreshScope刷新机制深入探究
    目录梳理过程如下@RefreshScopeScopedProxyModeRefreshAutoConfigurationNacosConfigServiceClientWorkerC...
    99+
    2023-03-12
    SpringCloud @RefreshScope SpringCloud刷新机制
  • SpringMvc定制化深入探究原理
    目录一、SpringBoot 自动配置套路二、定制化常见方式@EnableWebMvc 原理三、使用 @EnableWebMvc 案例一、SpringBoot 自动配置套路 引入场景...
    99+
    2022-11-13
    SpringMvc定制化 SpringMvc定制化原理 SpringBoot定制化
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作