系统性Node.js——手写文件流

手写文件流 主要分为 「可读流」 与 「可写流」 , 在 Node 中 stream 模块封装了流的基本操作 。 我们今天主要介绍的文件流也是继承 stream 模块来实现的 。
文件流 针对文件操作而实现的流 。 当我们操作文件时 , 由于文件可能特别大 , 如果一次性操作文件的所有内容 , 性能跟内存消耗肯定会很高 。 所以我们可以像流水一样 , 一点一点的读取并操作文件 , 这样每次消耗的性能跟内存会很少 , cpu 也会有时间去处理其它的任务 。
文件可读流读取文件 , 将文件内容一点一点的读入内存当中 。
使用方式我们先看一下基本的使用方式 。
const fs = require('fs')const rs = fs.createReadStream('./w-test.js')rs.on('data', (chunk) => {console.log(chunk)})rs.on('close', () => {console.log('close')})如上代码所示 , 我们通过 fs.createStream() 创建了一个可读流 , 用来读取 w-test.js 文件 。
当 on('data') 时 , 会自动的读取文件数据 , 每次默认读取 64kb 的内容 , 也可以通过 highWaterMark 参数来动态改变每次内容流程的阈值 。
文件读取完毕后会自动触发 close 事件 。
如下代码为 createReadStream 可以配置的参数
const rs = fs.createReadStream('./w-test.js', {flags: 'r', // 文件系统表示 , 这里是指以可读的形式操作文件encoding: null, // 编码方式autoClose: false, // 读取完毕时是否自动触发 close 事件start: 0, // 开始读取的位置end: 2, // 结束读取的位置highWaterMark: 2 // 每次读取的内容大小})「注意:」 start 跟 end 都是包含的 , 即 [start, end] 。
其实 , fs.crateReadStream 就是返回一个 fs.ReadStream 类的实例 , 所以上述代码就等同于:
const rs = new fs.ReadStream('./w-test.js', {flags: 'r', // 文件系统表示 , 这里是指以可读的形式操作文件encoding: null, // 编码方式autoClose: false, // 读取完毕时是否自动触发 close 事件start: 0, // 开始读取的位置end: 2, // 结束读取的位置highWaterMark: 2 // 每次读取的内容大小})手写文件可读流了解完使用方式 , 那我们就应该尝试从原理上去搞定它 , 接下来 , 我们手写一个可读流 。
初始化首先 , ReadStream 是一个类 , 从表现上来看这个类可以监听事件即 on('data') , 所以我们应该让它继承自 EventEmitter , 如下代码:
class ReadStream extends EventEmitter {constructor() {super();}}然后我们初始化参数 , 并打开文件 , 如下代码(代码中会对关键代码作注释):
class ReadStream extends EventEmitter {constructor(path, options = {}) {super()this.path = paththis.flags = options.flags ?? 'r'this.encoding = options.encoding ?? 'utf8'this.autoClose = options.autoClose ?? truethis.start = options.start ?? 0this.end = options.end ?? undefinedthis.highWaterMark = options.highWaterMark ?? 16 * 1024// 文件的偏移量this.offset = this.start// 是否处于流动状态 , 调用 pause 或 resume 方法时会用到 , 下文会讲到this.flowing = false// 打开文件this.open()// 当绑定新事件时会触发 newListener// 这里当绑定this.on('newListener', (type) => {if (type === 'data') {// 标记为开始流动this.flowing = true// 开始读取文件this.read()}})}}