Node:fs可读流重新学习

52 阅读4分钟

一、需求分析

我们先来说说需求:将a.txt中的文本复制到b.txt中去。

二、普通fs完成需求

如果按照一般写法来说就是利用fs.read,fs.write直接读取内容然后写入内容,但这样会导致一个问题,a.txt文件大小有多大,我们在读写的时候就需要消耗多大的资源,但是我们不希望它占用太多资源

优化思路:指定缓冲区大小,只在缓冲区大小范围内进行多次读写,我们可以利用递归思路这样封装

const fs = require('fs')
function copy(source, target, cb) {
    const BUFFER_SIZE = 3
    const buffer = Buffer.alloc(BUFFER_SIZE)
    const r_offset = 0
    const w_offset = 0
    //读入一部分数据就写入一部分数据
    fs.open(source, 'r', function (err, rfd) {
        fs.open(target, 'w', function (err, wfd) {
            function next() {
                fs.read(rfd, buffer, 0, BUFFER_SIZE, r_offset, function (err, bytesRead) {
                    if (err) return cb(err)
                    if (bytesRead) {
                        fs.write(wfd, buffer, 0, bytesRead, w_offset, function (err, written) {
                            r_offset = bytesRead
                            w_offset = written
                            nextTick()
                        })
                    } else {
                        fs.close(rfd, () => { })
                        fs.close(wfd, () => { })
                        cb()
                    }
                })
            }
            next()
        })
    })
}

copy('./a.text', './b.text', function (err) {
    if (err) return console.log(err)
    console.log('copy success')
})

三、fs可读流

从代码风格上来看,此时fs的读写全耦合在一块,有没有更为简洁的书写方式呢,我们可以通过可读流来解决,可读流不是一下子将文件都读取完毕,而是可以控制读取的个数和读取的速率,同时代码风格也相当简洁,下面是基础用法

//可读流 不是一下子将文件都读取完毕,而是可以控制读取的个数和读取的速率

const fs = require('fs')

let rs = fs.createReadStream('./a.text', {
    flags: 'r',
    encoding: null,//编码就是buffer
    autoClose: true,//相当于需要调用close方法
    start: 0,
    highWaterMark: 3 //每次读取数据的个数,默认是64*1024字节
})
//监听用户,绑定了data事件,会触发对应的回调,不停的触发

rs.on('open', function (fd) {
    console.log(fd)
})

rs.on('data', function (chunk) {
    console.log(fd)
    rs.pause()  //不再触发data事件
})

rs.on('end', function () {
    console.log('end')
})

rs.on('close', function () {
    console.log(close)
})

setInterval(() => {
    rs.resume()//再次触发data事件
}, 1000)

四、手写可读流

1、初始化可读流属性

const EventEmitter = require('events')
const fs = require('fs')
class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
        super()

        this.path = path
        this.flags = options.flags || 'r'
        this.encoding = options.encoding || null
        this.autoClose = options.autoClose || true
        this.start = options.start || 0
        this.end = options.end
        this.highWaterMark = options.highWaterMark || 64 * 1024

    }
 }
 module.exports = ReadStream

2、open()与read()

class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
        //........  省略部分代码
        this.end = options.end
        this.highWaterMark = options.highWaterMark || 64 * 1024
        this.open() //文件打开操作,这个方法是异步的
        this.read()
        
    }
     open() {
        fs.open(this.path, this.flags, (err, fd) => {
            this.fd = fd
            this.emit('open', fd)
        })
    }
    read(){
        console.log(fd)
    }
 }
 module.exports = ReadStream

此时会出现一个问题,read()该如何从open方法中拿到fd,open方法本身为异步方法,read()无法保证在open()后获取fd,并且必须在再监听到data事件后才开始read()方法

2.1处理open异步,data事件后read问题,open()方法报错

this.on('newListener',(type)=>{})可以监听每次新绑定的事件,当type === 'data'再去执行read()

3、通过发布订阅的方式处理open异步问题

const EventEmitter = require('events')
const fs = require('fs')
class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
    //........  省略部分代码
        this.open() //文件打开操作,这个方法是异步的

        //注意用户监听了data事件,才需要读取
        this.on('newListener', function (type) {
            if (type === 'data') {
                this.read()
            }
        })
    }
    read() {//once events 模块中的绑定一次
        //希望在open之后才能拿到fd
        if (typeof this.fd !== 'number') {
            return this.once('open', () => { this.read() })
        }
        console.log(this.fd)
    }
    destory(err) {
        if (err) {
            this.emit('error', err)
        }
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                return this.destory(err)
            }
            this.fd = fd
            this.emit('open', fd)
        })
    }

module.exports = ReadStream

4、完善read()方法

read方法流程:

fd为空,则去调用open方法,fd不为空,说明open方法已经调用

分配读空间的缓存区,每次读取hightWaterMark大小的数据,递归调用read方法,读取完所有数据后,调用end

 read() {//once events 模块中的绑定一次
        //希望在open之后才能拿到fd
        if (typeof this.fd !== 'number') {
            return this.once('open', () => { this.read() })
        }
        const buffer = Buffer.alloc(this.highWaterMark)
        fs.read(this.fd, buffer, 0, this.highWaterMark, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead
                this.emit('data', buffer.slice(0, bytesRead))
                this.read()
            } else {
                this.emit('end')
            }
        })
    }

4.1特殊情况处理:

当用户设定了读取的start和end时,每次读取大小不一定为hightWaterMark,判断边界条件:Math.min(this.end - this.offset + 1, this.highWaterMark)

tip: +1是因为end包后

   read() {//once events 模块中的绑定一次
        //希望在open之后才能拿到fd
        if (typeof this.fd !== 'number') {
            return this.once('open', () => { this.read() })
        }
        let howMutchToRead = this.end ? Math.min(this.end - this.offset + 1, this.highWaterMark) : this.highWaterMark
        const buffer = Buffer.alloc(howMutchToRead)
        fs.read(this.fd, buffer, 0, howMutchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead
                this.emit('data', buffer.slice(0, bytesRead))
                this.read()
            } else {
                this.emit('end')
                this.destory()
            }
        })
    }
    destory(err) {
        if (err) {
            this.emit('error', err)
        }
        if (this.autoClose) {
            fs.close(this.fd, () => { this.emit('close') })
        }
    }

5、pause(),resume()


class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
    //......省略部分代码
        this.flowing = false // pause resume
//......省略部分代码
    }
    resume() {
        if (!this.flowing) {
            this.flowing = true
            this.read()
        }
    }
    pause() {
        this.flowing = false
    }
    read() {//once events 模块中的绑定一次
        fs.read(this.fd, buffer, 0, howMutchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
               //......省略部分代码
                if (this.flowing) {
                    this.read()
                }
            } 
        })
    }

//......省略部分代码
}


五、完整代码

const EventEmitter = require('events')
const fs = require('fs')
class ReadStream extends EventEmitter {
    constructor(path, options = {}) {
        super()

        this.path = path
        this.flags = options.flags || 'r'
        this.encoding = options.encoding || null
        this.autoClose = options.autoClose || true
        this.start = options.start || 0
        this.end = options.end
        this.highWaterMark = options.highWaterMark || 64 * 1024

        this.flowing = false // pause resume

        this.open() //文件打开操作,这个方法是异步的

        //注意用户监听了data事件,才需要读取
        this.on('newListener', function (type) {
            if (type === 'data') {
                this.flowing = true
                this.read()
            }
        })
        this.offset = this.start
    }
    resume() {
        if (!this.flowing) {
            this.flowing = true
            this.read()
        }
    }
    pause() {
        this.flowing = false
    }
    read() {//once events 模块中的绑定一次
        //希望在open之后才能拿到fd
        if (typeof this.fd !== 'number') {
            return this.once('open', () => { this.read() })
        }
        let howMutchToRead = this.end ? Math.min(this.end - this.offset + 1, this.highWaterMark) : this.highWaterMark
        const buffer = Buffer.alloc(howMutchToRead)
        fs.read(this.fd, buffer, 0, howMutchToRead, this.offset, (err, bytesRead) => {
            if (bytesRead) {
                this.offset += bytesRead
                this.emit('data', buffer.slice(0, bytesRead))
                if (this.flowing) {
                    this.read()
                }
            } else {
                this.emit('end')
                this.destory()
            }
        })
    }

    destory(err) {
        if (err) {
            this.emit('error', err)
        }
        if (this.autoClose) {
            fs.close(this.fd, () => { this.emit('close') })
        }
    }
    open() {
        fs.open(this.path, this.flags, (err, fd) => {
            if (err) {
                return this.destory(err)
            }
            this.fd = fd
            this.emit('open', fd)
        })
    }
}

module.exports = ReadStream