第七十六期:Node中的streams流(管道方法pipe)

这里记录工作中遇到的技术点,以及自己对生活的一些思考,周三或周五发布。

封面图

Node中的streams流

streams流是Node中的最好的特性之一。它在我们的开发过程当中可以帮助我们做很多事情。比如通过流的方式梳理大量数据,或者帮我们分离应用程序。

和streams流相关的内容有哪些呢?大致有这么几点:

  • 处理大量数据
  • 使用管道方法
  • 转换流
  • 读写流
  • 解耦I/O

处理无限量的数据

使用data事件,我们可以在消耗很少内存的情况下去处理一小块文件。

我们其实也可以处理无限量的数据,比如:我们可以从伪随机数生成器中读取字节数。

const rs = fs.createReadStream('/dev/urandom')
let size = 0
rs.on('data', (data) => {
  size += data.length
  console.log('文件大小:', size)
})

复制代码

结果如下:

尽管程序一直在执行,并且文件的数量也是无限的,但是程序并没有崩溃。

可伸缩性是流的特性之一,大多数使用流编写的程序都可以很好的伸缩任何输入的大小。

flow模式(flow Mode)与pull-base模式(pull-based stream)

Node中的流可以处于流模式或者pull-based模式。当我们将数据添加到流,它就进入flow模式,这表示:只要有数据,就会调用data事件。

在上面的示例代码中,readStream刚刚创建的时候,并不处于flow模式,我们通过data事件将它放置到flow模式。

如果我们想停止它,我们可以调用可读流的暂停方法pause()。如果我们想重新开启它,我们可以调用resume()方法。

但是flow模式也可能会有问题,因为在某些情况下,即使流暂停,流也可能被传入数据的淹没,传入流可能不收pause()方法控制。

从流中提取数据的另一种方法是等待readable事件,然后不断调用流的read方法,直到返回null(即流终止符实体)。通过这种方式,我们可以从流中提取数据,并且可以在必要时停止提取。

换句话说,我们不需要指示流暂停然后继续;我们可以根据需要启动或者停止它。

看个例子:

const fs = require('fs')
const rs = fs.createReadStream(__filename)
rs.on('readable', () => {
  let data = rs.read()
  while (data !== null) {
    console.log('读取的数据:', data)
    data = rs.read()
  }
})

rs.on('end', () => {
  console.log('没有数据了')
})
复制代码

这个例子我们通过readable事件去判断是否有数据,而不是直接调用data事件。

当然,从流中提取数据更好的方法是通过pipe(管道)将我们的数据传输到我们创建的流中。这样一来管理内存的问题就可以在内部进行。

理解stream流的事件

所有流都继承自EventEmitter类并带有一系列不同的事件。了解一些我们经常用的事件,对于我们在处理流的过程当中非常有用。

第一,data事件。从可读流中读取新数据时触发。data数据作为事件处理程序的第一个参数。需要注意的是,与其他事件处理程序不同,附加数据侦听器会产生副作用。当连接第一个数据侦听器时,我们的流将被取消暂停。

第二,end事件。当可读流中没有数据时触发。

第三,finish事件。当可写流结束且所有挂起的写入都已完成时发出。

第四,close事件。通常在流完全关闭时发出,stream不一定会触发事件。

第五,puse事件。用于暂停一个可读流。大部分情况我们可以忽略这个方法。

第六,resume事件。用于重启一个可读流。

pipe方法

pipe方法用来将两个stream连接到一起。shell脚本中我们经常使用 | 管道符号来实现这个功能。通过这些方式,我们可以将多个管道连接在一起,更加方便的处理数据。

Streams的API 也为我们提供了pipe方法。每个可读都有一个pipe方法。

我们还是用一个例子来感受一下:

const zlib = require('zlib')
const map =  require('tar-map-stream')
const decompress = zlib.createGunzip()
const whoami = process.env.USER ||process.env.USERNAME

const convert = map((header)=>{
  header.uname = whoami
  header.mtime = new Date()
  header.name = header.name.replace('node-v0.1.100','edon-v0.0.0')
  return header
})
const compress = zlib.createGzip()

process.stdin.pipe(decompress)
  .pipe(convert)
  .pipe(compress)
  .pipe(process.stdout)
复制代码

然后执行:

curl https://nodejs.org/dist/v0.1.100/node-v0.100.tar.gz | node read.js > edon.tar.gz
复制代码

pipe方法将数据侦听绑定到streams流的源头,然后将接收到的数据导流到目标streams中。

当我们通过pipe将多个streams串联在一起时,我们是实际在告诉Node用这些流来解析数据。

使用pipe管道处理数据,比使用data方法相对来说更加安全一些,因为它可以自由的处理背压(backpressure),背压这个概念我们可以理解为内存管理。

比如,当快速生成数据的流可能会压到较慢的写入流时,需要使用缓冲压力策略来防止内存填满和进程崩溃。管道方法提供了这种背压。

上面的代码中,我们通过 | 管道符号将请求的数据导流到我们的 index.js 脚本中。

然后process.stdin标准I/O通过pipe方法一层一层的往下传递,最终通过重定向>存入edon.tar.gz文件中。

整个过程如下:

curl---> process.stdin---> decompress ---> content---> compress---> process.stdout---> endon.tar.gz

最后

  • 公众号《JavaScript高级程序设计》
  • 公众号内回复”vue-router“ 或 ”router“即可收到 VueRouter源码分析的文档。
  • 回复”vuex“ 或 ”Vuex“即可收到 Vuex 源码分析的文档。

全文完,如果喜欢。

请点赞和"在看"吧,最好也加个"关注",或者分享到朋友圈。

Supongo que te gusta

Origin juejin.im/post/7067043139680206856
Recomendado
Clasificación