文件可写流

预计阅读时间: 9 分钟

本篇来看一下文件的可写流操作,它是一个继承了 Writeable 与 EventEmitter 类的内置 api,可以去通过 fs 来创建使用。

write 操作

const fs = require('fs')

let ws = fs.createWriteStream('test.txt', {
  flags: 'w',
  mode: 0o666,
  encoding: 'utf-8', // 我们这里写文字进去肯定是以 utf-8 的格式
  fd: null,
  start: 0, // 从哪个位置开始写
  highWaterMark: 3 // 表示缓冲区的大小
})

ws.write('哇咔咔', () => {
  console.log('哇咔咔 写完了')
})

ws.write('abcd', () => {
  console.log('abcd 写完了')
})

可以看到使用基本和前几章的自定义 Writeable 是一样。

背压问题测试

再来看看下面这段代码。

const fs = require('fs')

let ws = fs.createWriteStream('test.txt', {
  highWaterMark: 3 // 中文一个字3个字节
})

let flag = ws.write('1')
console.log(flag) // true

flag = ws.write('2')
console.log(flag) // true

flag = ws.write('3')
console.log(flag) // false

结合前面可读流的背压问题看,如果 flag 为 false,并不是说明当前数据不能被执行写入,它类似于一种开关,它控制的是上游数据的产量。在上游会有一个数据的生产者就是可读流,下游会有一个数据的消费者就是可写流,然后最下边会有一个数据真正要存储的地方,比如说就是一个文件。我们这个生产者来负责去生产数据,然后它会去流向我们当前的这个可写流,它的内部也是有一片空间的,就是一个的缓存区域,它有大小的上限,默认情况下文件可写流,它并没有对这个大小进行修改,所以它就是一个 16kb 的,上面的代码是把它改为了 3 个字节,这样就可以测试了。

紧接着我们去调用 write 方法,然后去把这个数据从这个缓存区里边,然后往这个文件里去执行写入操作,那这个时候就会有一个问题,我们需要去往这个缓存区里面放数据,而缓存区它是有大小上限的,但是我们又不知道前边的这个数据的生产者,到底要给我们多少的数据,所以这一层会有一个生产速度和消费速度之间的关系,生产速度和消费速度是不一样的,一般情况下生产速度要比消费速度快很多,所以这个时候我们就会有一个问题,比如说这个时候,highWaterMark 是设置为 3 个字节,那我们这个前边的生产者,假如说给出来了 5 个字节的数据要去执行写入,那在某一个时间点上,它会告诉我们是超过了这个水位线的,一旦说超过了这个水位线之后,我们当前的这个 write 所执行之后的结果就会返回一个 false 来作为告知,它告知的并不是说我们当前的数据没办法往文件里面写了,它只是起到了一定的警戒作用,不代表着说超过水位线,我们当前的水就一定会溢出,他仍然是可以把值先放在内存里边儿,但是它要告知我们当前的生产数据的这个人,我的消费速度已经跟不上你的生产速度了,所以这个时候生产者或者可读流的模式就会被修改为暂停模式,消费者会慢慢的消化它内部缓存中的数据,直到可以再次被执行写入操作。那么当缓冲区可以继续写入数据数据时,如何让生产者知道?那这个时候我们当前的这个文件可写流,就会有一个事件叫 drain 事件可以完成这个操作。

drain 事件

关于这个事件,经常会有一个疑问就是说这个事件它即使不被触发,我们的数据写入操作也是照样能完成的,那为什么还要多此一举来设计这个事件呢?在实际的应用中,drain 事件他的确也不是特别的常用,因为有一个更好的 pipe 方法来去处理数据,但是我们肯定也想知道 pipe 方法它到底是解决了什么问题,才做到了不需要去频繁的使用 drain 事件。这里的话我们就先用一个例子来尽可能的去把两者的关系去梳理一下,drain 事件就好比是我们在路口看到的警察叔叔,在红绿灯没有坏的时候,它是不需要出来营业的,因为再大的车流量,在系统的指挥下,他都能够去有条不紊地被消费掉,显然 pipe 方法就是这里的红绿灯系统,他可以去把所有的事情都搞定,但是如果说哪天这个系统它坏掉了,那这个时候警察叔叔就要出来营业了,然后他会按照实际的需求来控制各个路口的进入流量,最后再按照顺序一点一点的去进行放行。所以想明白了这个模型之后,我们再回头去想一想,drain 事件也许就会有一些眉目了,其实它主要的作用就是用于控制速度,或者说是按需来完成限流,具体的手段就是指定 highWaterMark,然后再去配合着 write 方法的返回值去进行使用。

那接下来通过以下代码来更深刻的看一下 drain 事件的使用操作。

const fs = require('fs')

let ws = fs.createWriteStream('test.txt', {
  highWaterMark: 3 // 中文一个字3个字节
})

const source = '哇咔咔'.split('')
let num = 0
let flag = true

function executeWrite() {
  while (num !== source.length && flag) {
    flag = ws.write(source[num])
    num++
  }
}

executeWrite()

ws.on('drain', () => {
  console.log('drain 事件触发了')
  flag = true
  executeWrite()
})

我们在最终会有一个更好的方法,就是 pipe 方法,它就是一个管道,我们只需要按照相应的类型去把可读流的东西去交给可写流,或者说交给转换流、双工流,然后它会自行的去进行处理并再继续流向下一个管道,但是在我们去大量的使用 pipe 之前,如果说我们把这个限流的这样一个过程,以及 drain 事件它干嘛用的去搞清楚之后,非常的有利于我们将来去看一看 pipe 方法的实现原理。