5. buffer&fs&stream

233 阅读19分钟

编码

buffer的概念

Buffer用于描述内存, 内存是2进制,buffer是16进制

  • 在服务端,我们需要一个东西可以来标识内存,但是不能是字符串,因为字符串无法标识图片
  • node 中用 Buffer 来标识内存的数据,把内容转换成了 16 进制来显示(因为 16 进制比较短)
  • 10 进制 255 => 2 进制 0b11111111 => 16 进制 0xff => buffer 每个字节的取值范围就是 0 - 0xff
  • JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,该类用来创建一个 专门存放二进制数据的缓存区
  • (utf-8) 1 个汉字 3 个字节
  • buffer 对象类似于数组,他的 元素都是 16 进制的两位数 ,即 0~255 的数值
  • node 中 buffer 和字符串任意转换 (可能会出现乱码)

编码

  • node的编码目前只支持utf8,不同的编码占用的字节数不同,对于utf8而言,一个汉字是3个字节
  • 字节是能看得到的最小单位
  • 位 1个字节由8个位组成(二进制)组成
  • 将任何进制转换成10进制: 当前位的值 * 当前位^(所在位-1)累加的结果 => 对应方法: parseInt("1010", 2);
  • 将整数转换成其他进制 不停取余反向输出 => 对应方法: 10..toString(2)

2进制以0b开头 8进制以0o开头 16进制以0x开头

  • 小数也要转化成 2 进制
    • 10 进制中的 0.5 是 2 进制中的 0.1(因为 10 => 0.5 20 倍 所以 2 => 0.1 20 倍)
    • 十进制小数转为二进制的方法:乘 2 取整法可以将一个小数转化成 2 进制数
// 0.1 + 0.2的问题
// 计算并不是直接相加,而是把两个值都转化成2进制来计算

0.1 * 2 = 0.2 0
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1
0.1 * 2 = 0.2 0
0.2 * 2 = 0.4 0
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1

// 0.1转为二进制是一个无穷尽的小数
// 0.1转为二进制进行存储的时候会稍微比0.1大一点
// 0.2也是这样
// 所以 0.1 + 0.2 会大于 0.3

console.log(0.1+0.2) // 考察的是进制转化的问题

// js没有将小数转化成2进制方法
// 为什么 0.2+0.2 没有问题?

// 如果出现精度问题怎么解决?

编码规范

  • ASCII
    • 一个字节最大是255,也就是可以代表255个数,大小写和一些特殊符号 最多就用了127个,一个字节就搞定了所有编码
  • GB8030/GBK
    • 255 对于中文是不够用的
    • 从127之后编制自己的, 不包含其他国家的
  • unicode
    • 组织 一套编码
  • UTF8
    • 借用unicode实现自己的一套机制
    • 如果是字符还是要遵循ASCII
    • 字符集
  • node 不支持 gbk,只支持 utf8

常见的编码实现--base64

由于某些系统中只能使用ASCII字符。Base64就是用来将非ASCII字符的数据转换成ASCII字符的一种方法。

  • base64 编码 : base64 只是一个编码规则,没有加密功能

  • base64 字符串可以放到任何路径的链接里,可以减少请求的发送,但是文件大小会变大,base64 转化完毕后会比之前的文件大 1/3

  • base64 的来源就是将每个字节都转化成小于 64 的值

  • base64原理

    • 一个汉字有3个字节,24位
    • 把一个汉字的24位 转换成4个字节, 每个字节就6位, 但是一个字节应该是有8位,所以不足的补0(注意是在前面补0)
// base64的方法
// 调用toString转化成指定编码
// 汉字“珠” 是3个字节 转换后是 54+g 是4个字节 比之前打了1/3
const r = Buffer.from('珠').toString('base64');
/*
进制转换

base64 是一种进制转换
buffer里就有一个方法可以将buffer转换成base64
let buffer = Buffer.from("珠");
buffer.toString("base64")

-1.buffer是16进制,要将16进制转换成2进制 => toString()
(0xe7).toString(2);
(0x8f).toString(2);
(0xa0).toString(2);
// 11100111 10001111 10100000
=> 转换成4个字节, 每个字节就6位,
// 111001 111000 111110 100000
// 00111001 00111000 00111110 00100000 (前两位定死都是00, 最大值是00111111十进制就是63, 即转换后的值不会大于64)
-2.将这些值转化成10进制,去可见编码中取值 => parseInt()


parseInt("00111001", 2) 或者 parseInt(0b00111001) //57
parseInt("00111000", 2) //56
parseInt("00111110", 2) //62
parseInt("00100000", 2) //32


可见编码: 标准base64只有64个字符(英文大小写,数字,+, /)
let str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
str += "abcdefghijklmnopqrstuvwxyz";
str += "0123456789";
str += "+/";
console.log(str[57] + str[56] + str[62] + str[32]) // 就得到转化而成的base64编码

base64就是做了编码映射 并没有加密功能
*/

Buffer

  • buffer 代表的是内存 内存是一段“固定空间” 产生的内存是固定大小 不能随意添加
  • 如果想要更改buffer的大小,改小可以截取内存,改大的话需要创造一个大的内存空间,将数据拷贝过去
  • 一般会用 alloc 声明一个 buffer 或者把字符串转换成一个 buffer
const buffer1 = Buffer.alloc(5);

// 像数组,但是和数组有区别,数组可以扩展,buffer不能扩展,可以用索引取值
console.log(buffer[0]); // 打印的是当前buffer里的这个字节所代表的十进制是多少

// 非常少使用,一般不会直接填16进制
const buffer2 = Buffer.from([0x24, 0x25, 0x26]);

const buffer3 = Buffer.from('哈哈'); // 对应6个字节
  • 后台获取的数据都是 buffer,包括文件操作也都是 buffer 形式

定义 buffer 的三种方式

  1. let buf1 = Buffer.alloc(6);
  2. let buf2 = Buffer.from('哈哈');
  3. let buf3 = Buffer.from([65,66,67]);
  • 通过长度定义 buffer

    let buffer = Buffer.alloc(6)

    指定buffer大小, 参数是数字,最小是3, 数字指的是字节

    let buffer = Buffer.allocUnsafe(6)

  • 通过数组定义 buffer

    let buffer = Buffer.from([1,2,3,4]) 会自动将数组中的十进制数转化成 16 进制

    参数是数组,告诉要存入buffer的内容是什么,使用场景很少 ,数组里只能放数字0-255,超过255会取余

  • 字符串创建 buffer -- 用得最多

    let buffer = Buffer.from('哈哈') => 字符串转buffer

    buffer.length 指的是转换成 16 进制的 buffer 后的长度

    buffer.toString() => 将 buffer 转换成字符串 buffer.toString() 不传参数默认是utf8

buffer 方法

  • buffer 功能是用来存储二进制数据,buffer 中的每一位都表示的是一个字节,如果显示成二进制,每一个字节就是显示成 8 位, 让其每个字节显示成 16 进制,就是显示两位,比较短,这就是 buffer 每个元素(每个字节)显示成 16 进制的原因
  • 无论是 2 进制还是 16 进制,他们表现的东西都是一样的
  • buffer 常用的方法

    buffer.fill 填充 buffer 中的内容

    buffer.toString 将 buffer 转化为字符串

    buffer.slice 截取想要的 buffer

    buffer.copy 拷贝 buffer

    buffer.concat buffer 的拼接方法

    buffer.isBuffer 判断是否是 buffer 类型

var buffer = Buffer.from([1, 2, 3]);
var newBuffer = buffer.slice(0, 1); // 拷贝出来的存放的是内存地址空间
newBuffer[0] = 100; //修改newBuffer,buffer也跟着修改

var buf1 = Buffer.from('嘻嘻');
var buf2 = Buffer.from('哈哈');
var buf = Buffer.allocUnsafe(12);
// 拷贝buffer(copy)
// targetBuffer目标buffer targetStart目标的开始 sourceStart源的开始 sourceEnd源的结束 this.length
// 将buf1/buf2拷贝到buf
buf1.copy(buf, 0);
buf2.copy(buf, 6);
console.log(buf, buf.toString());
//但这种做法 如果buffer很长,还要计算复制的位置,而且buffer定了就不能改变大小,
//所以concat更方便 ,内部自动开辟两个要拼接的buffer空间的总和然后进行拼接(concat的原理就是copy)
//连接buffer
Buffer.concat([buf1, buf2]).toString();

Buffer.prototype.MyCopy = function (
  targetBuffer,
  targetStart,
  sourceStart = 0,
  sourceEnd = this.length
) {
  for (let i = sourceStart; i < sourceEnd; i++) {
    targetBuffer[targetStart++] = this[i];
  }
};

Buffer.MyConcat = function (list, totalLength) {
  // 判断长度是否传递,如果传递了就用,没传自己算一个总长度
  if (typeof totalLength === 'undefined') {
    totalLength = list.reduce((prev, next) => prev + next.length, 0);
  }
  // 通过长度创建一个这么大的buffer, Buffer.alloc(len)
  let buffer = Buffer.alloc(totalLength);
  // 再循环list将每一项拷贝到这个大buffer上 buf.copy
  let offset = 0;
  list.forEach((buff) => {
    if (!Buffer.isBuffer(buff)) return;
    buff.copy(buffer, offset);
    offset += buff.length;
  });
  // 如果长度过长fill 或者采用slice截取有效长度
  // 返回一个新的buffer
  return buffer.slice(0, offset);
};

fs

  • I/O input output 读文件 => 写操作 (以内存为参照物)
  • 读取的时候默认不写编码是 buffer 类型,如果文件不存在则报错
  • 写入的时候默认会将内容以 utf8 格式写入,如果文件不存在会创建(会将内容自动 toString('utf8'))
  • 读取的概念是将读取到的文件内容放到内存中,写入是读取内存中的内容写入到文件中

文件状态判断 -- fs.stat

fs.stat('1.txt', function (err, stats) {
  if (err)
    // 文件不存在
    stats.isFile(); //是否是文件
  stats.isDirectory(); // 是否是文件夹
});

创建文件夹 -- fs.mkdir

// 不能跳级创建文件夹 创建如下文件夹d的前提是 a/b/c存在
fs.mkdir('a/b/c/d', function (err) {});
// 递归创建文件夹
function makep(url, cb) {
  let urlArr = url.split('/');
  let index = 0;
  function make(p) {
    if (urlArr.length < index) return; // 终止循环
    // 在创建之前看是否存在,如果不存在就创建,存在继续下一次创建
    fs.stat(p, function (err) {
      if (err) {
        //文件不存在
        fs.mkdir(p, function (err) {
          index++;
          make(urlArr.slice(0, index + 1).join('/'));
        });
      } else {
        // 如果存在跳到下一次创建
        make(urlArr.slice(0, ++index + 1).join('/'));
      }
    });
  }
  make(urlArr[index]);
}
makep('a/b/c/d', function (err) {
  console.log('创建成功');
});

读取文件夹

fs.readdir('a', function(err,dirs){}) // 只能读取一层

删除文件夹

fs.rmdir("a", function(err){}) // 删除文件夹,当文件夹下面有内容的时候不让删除

自实现删除文件夹

  • 异步删除多层目录的功能
    • 串行删除
    • 并发删除 同时删除子文件夹 删除后 再删除自己
    • 广度遍历 再倒序删除
// 串行删除
function rmdir(dir, cb) {
    fs.stat(dir, function(err, stat) {
        if(stat.isDirectory()) {
            fs.readdir(dir, function(err, dirs) {
                dirs = dirs.map(item => path.join(dir, item));
                let index = 0;
                function step()  {
                    if(index ===  dirs.length) return fs.rmdir(dir, cb);
                    rmdir(dirs[index++], step)
                }
                step()
            })
        }else{
            fs.unlink(dir, cb)
        }
    })
}
// 串行promise写法
const fs = require("fs").promises;
const path = require("path");
async function rmdir(dir) {
    const statObj = await fs.stat(dir);
    if(statObj.isDirectory()) {
        let dirs = await fs.readdir(dir);
        dirs = dirs.map(item => path.join(dir, item));
        for(let i = 0; i<dirs.length; i++) {
            await rmdir(dirs[i]);
        }
        await fs.rmdir(dir);
    }else{
        return fs.unlink(dir);
    }
}
rmdir("aaa").then(() => {
    console.log("success zhuzhu")
})


// 并发删除
function rmdir(dir, cb) {
    fs.stat(dir, function(err, statObj)  {
        if(stat.isDirectory()) {
            fs.readdir(dir, function(err, dirs) {
                dirs = dirs.map(item => path.join(dir, item));
                if(!dirs.length) {
                    return fs.rmdir(dir, cb);
                }
                let v = 0;
                function done() {
                    if(++v === dirs.length) {
                        return fs.rmdir(dir, cb)
                    }
                }
                for(let i  = 0; i < dirs.length; i++) { // 并发执行
                    rmdir(dirs[i], done)
                }
            })
        }else{
            fs.unlink(dir, cb)
        }
    })
}

// 并发promise的写法

const fs = require("fs").promises;
const path = require("path");
async function rmdir(dir) {
   const statObj = await fs.stat(dir);
   if(statObj.isDerectory()) {
       let dirs = await fs.readdir(dir);
       await Promise.all(dirs.map(item => rmdir(path.join(dir, item))));
       await fs.rmdir(dir);
   }else{
       return fs.unlink(dir);
   }
}

// 串行广度遍历
const fs = require("fs").promises;
const path = require("path");
async function rmdir(dir) {
    let stack = [dir];
    let index = 0;
    let currentNode;
    while(currentNode = stack[index++]){ // 广度遍历,放入所有的文件夹和文件
        let statObj = await fs.stat(currentNode);
        if(statObj.isDirectory()) {
            let dirs = await fs.readdir(currentNode);
            dirs = dirs.map(item => path.join(currentNode, item));
            stack = [...stack, ...dirs];
        }
    }

        // 倒序删除
    let len = stack.length;
    while(len--) {
        let stat = await fs.stat(stack[len]);
        if(stat.isDirectory()) {
            await fs.rmdir(stack[len])
        }else{
            await fs.unlink(stack[len])
        }
    }
}

rmdir("aaa").then(() => {
    console.log("success zhuzhu")
})

读取文件

  1. 去读文件 文件必须存在 不能通过/读取内容, /表示的是根目录
  2. 读取的默认类型是 buffer
let result = fs.readFileSync('log.js');
// 两种方式转result的格式:
result.toString();
fs.readFileSync('log.js', 'utf8');

写入文件

  1. 读取类型都是 buffer, 写入的时候都是 utf8
  2. 读的文件必须存在, 写的时候文件不存在会自动创建,里面的内容会被覆盖掉
  3. 会对第二个参数(文件内容)默认调用 toString 方法【读取文件是 buffer, 调用 toString 可转为 utf8】
fs.writeFile('1.txt', 文件内容, (err) => {});
fs.writeFileSync('1.txt', 文件内容);

读写文件(拷贝)

// 实现同步/异步读写文件函数
let fs = require("fs");

fucntion copySync(source, target) { // 同步 readFileSync + writeFileSync
    let result = fs.readFileSync(source, "utf8");
    fs.writeFileSync(target, result);
}
function copy(source, target, callback){ // 异步 readFile + writeFile
    fs.readFile(source, function(err, data) {
        if(err) return callback(err)
        fs.writeFile(target, data, callback)
    })
}
copy("1.txt", "2.txt", function(res) {
    console.log(res);
})
  • 上面的写法只能读取完毕后再次写入,此方式适合小文件。大文件使用此方法会导致淹没可用内存
  • 边读边写
// 可忽略此代码块

// 实现边读边写 每次读三个  a.txt:123456789
const fs = require('fs');
const path = require('path');
let buf = Buffer.alloc(3);
fs.open(path.resolve(__dirname, 'a.txt'), 'r', function (err, fd) {
  // fd:file descriptor 是一个number类型,文件描述符 用完要关闭掉
  console.log(fd, 'fd');
  // 读取a.txt 将读取到的内容写入buffer的第0个位置,写3个,从文件的第6个位置开始写入
  fs.read(fd, buf, 0, 3, 6, function (err, bytesRead) {
    // bytesRead是读取到的真正个数
    console.log(bytesRead, 'bytesRead', buf);
    // 打开进行w操作就会将文件清空 写入的永远是前3个字节
    fs.open(path.resolve(__dirname, 'b.txt'), 'w', function (err, wfd) {
      // 将buffer的数据从0开始读取3个 写入文件的第0位置
      fs.write(wfd, buf, 0, 3, 0, function (err, written) {
        console.log(written);
        // 内部还要递归.....
        // 还得关闭文件
        fs.close(fd, () => {});
        fs.close(wfd, () => {});
      });
    });
  });
});
  • w 写入操作 r 读取操作 a 追加操作 r+ 以读取为准可以写入操作 w+以写入为准可以执行读取操作
  • 权限: 3 组(当前用户的权限 用户所在组的权限 其他人权限) rwx 的组合(可读可写可执行) 421 => 7 => 最高权限 777(8 进制)
  • fs.open(source,"r",0o666, function(){}) // 默认权限是 0o666 表示可读可写

基于文件系统操作的流

  • 下面的实现读和写耦合在了一起
  • fs是文件流,是文件操作中自己实现的流。文件流是继承于stream的,底层的实现用的就是fs.open fs.read fs.write...
// 实现边读边写,一次拷贝三个字节
const fs = reuqire('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) {
      //  回调的方式实现功能 需要用递归
      // 同步代码 可以采用while循环
      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;
                  next();
                }
              );
            } else {
              fs.close(rfd, () => {});
              fs.close(wfd, () => {});
              cb();
            }
          }
        );
      }
      next();
    });
  });
}
copy('./a.txt', './b.txt', function (err) {});

为了将读和写解耦, 采取发布订阅模式, 有了可读流/可写流

可读流

  • buffer 中的一个字节如果显示的是 30 => 16 进制的 30 转 10 进制:48 => 在 ascii 码中找到对应的
  • 流的概念和 fs 没有关系
  • fs 基于 stream 模块底层扩展了一个文件的读写方法
  • fs.createReadStream() 创建一个可读流对象 rs,rs.on 可以监听事件
  • 如果不监听“data”事件,是非流动模式,相当于买了个水管,但是水不会流出来
  • 当监听了“data”事件,就从非流动模式=>流动模式, 会不停的触发 data 事件的回调函数, 每次都拿到 highWaterMark 设置的值大小的数据
  • 当文件读取完毕后会触发 end 事件
  • 所以流是基于事件的
  • 可读流具备的方法:data/end/error/resume/pause, 只要具备这些方法就称为可读流
  • open 和 close 是文件流独有的方法

可读流的使用

const fs = require('fs');
// 返回一个可读流对象
const rs = fs.createReadStream('./a.txt', {
  // 创建可读流一般不用自己传递参数
  flags: 'r', // r读取 给fs.open用的
  encoding: null, // 默认读取出来的就是buffer
  autoClose: true, // 读取完毕后需要关闭流 fs.close
  emitClose: true, // 读取完毕后触发一个close事件
  start: 0,
  highWaterMark: 3, // 每次读取的数据个数 默认是64 * 1024 字节
});
// 如果不监听“data”事件,是非流动模式,相当于买了个水管,但是水不会流出来
// 会监听用户,绑定了data事件就会触发对应的回调,不停的触发
// open/close是文件流独有的
rs.on('open', function (fd) {
  // 打开文件事件
  console.log(fd);
});
rs.on('data', function (chunk) { // 如果绑定了data事件会不停的触发data
事件将内部的数据传递出来
  // 读取文件事件
  console.log(chunk);
  rs.pause(); // 暂停 不继续触发data事件
});
rs.on('end', function () {
  // 文件读取完毕事件
  console.log('end');
});
rs.on('close', function () {
  // 关闭文件事件
  console.log('close');
});
rs.on('error', function (err) {
  console.log(err, 'error');
});

setInterval(() => {
  rs.resume(); // 恢复触发data事件
}, 1000);

可读流的实现

这里用到的思路值得学习

  • open 方法是异步的, 实现当 open 执行完成后再调用 read 方法读取文件
const fs = require('fs');
const EventEmitter = require('events');
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.open(); //  文件打开操作 注意这个方法是异步的

    // 注意用户监听了data事件才需要读取文件
    // this.read();

    // EventEmitter提供的,只要绑定方法,就在内部emit newListener
    // 这样就能监听到用户绑定了哪些事件,一绑定就触发
    // newListener的实现是:当绑定的方法名不是newListener,就触发newListener执行
    this.on('newListener', function (type) {
      console.log(type); // 当绑定某个事件的时候就会触发newListener事件
      if (type === 'data') {
        this.read();
      }
    });
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        return this.destroy(err);
      }
      this.fd = fd; // 供read方法使用
      this.emit('open', fd); // 触发open事件,这时候文件打开完成,可以拿到fd
    });
  }
  read() {
    // read方法需要拿到fd
    // 要在open异步回调执行后才能拿到fd
    if (typeof this.fd !== 'number') {
      // return this.once("open", (fd)  => {
      //     console.log(fd);
      // })
      return this.once('open', () => this.read());
    }
    console.log(this.fd); //一定能拿到fd
  }
  destroy(err) {
    if (err) {
      this.emit('error', err);
    }
  }
}
module.exports = ReadStream;
// 完整实现
const fs = require('fs');
const EventEmitter = require('events');
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.offset = this.start;
    
    // 开关
    // false非流动模式,还没有开始读取文件中的内容。后续pause、resume就是更新这个flowing属性 
    this.flowing = false;

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

    // 注意用户监听了data事件才需要读取文件
    // this.read();

    // EventEmitter提供的,只要绑定方法,就在内部emit newListener
    // 这样就能监听到用户绑定了哪些事件,一绑定就触发
    // newListener的实现是:当绑定的方法名不是newListener,就触发newListener执行
    this.on('newListener', function (type) {
      console.log(type); // 当绑定某个事件的时候就会触发newListener事件
      if (type === 'data') {
        this.flowing = true;
        this.read();
      }
    });
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        return this.destroy(err);
      }
      this.fd = fd; // 供read方法使用
      this.emit('open', fd); // 触发open事件,这时候文件打开完成,可以拿到fd
    });
  }
  read() {
    // read方法需要拿到fd
    // 要在open异步回调执行后才能拿到fd this.open是异步的
    if (typeof this.fd !== 'number') {
      // return this.once("open", (fd)  => {
      //     console.log(fd);
      // })
      return this.once('open', () => this.read());
    }
    console.log(this.fd); //一定能拿到fd
    const buffer = Buffer.alloc(this.highWaterMark);
    // 读取文件中的内容,每次读取this.highWaterMark个
    let howMuchToRead = this.end
      ? Math.min(this.end - this.offset + 1, this.highWaterMark)
      : this.highWaterMark;
    fs.read(this.fd,buffer,0,howMuchToRead,this.offset,(err, bytesRead) => {
        if (bytesRead) {
          this.offset += bytesRead;
          this.emit('data', buffer.slice(0, bytesRead));
          if (flowing) {
            this.read(); // 继续读
          }
        } else {
          this.emit('end');
          this.destroy();
        }
      }
    );
  }
  destroy(err) {
    if (err) {
      this.emit('error', err);
    }
    if (this.autoClose) {
      fs.close(this.fd, () => this.emit('close'));
    }
  }
  pause() {
    this.flowing = false;
  }
  resume() {
    if (!this.flowing) {
      this.flowing = true;
      this.read();
    }
  }
}
module.exports = ReadStream;

实现过程小结:

构造函数中设置默认参数,直接把文件打开,并且监听用户的事件,如果用户绑定了 data 事件就开始读取

但是在读取文件的过程中可能文件还没有打开,就绑定一个 open 事件,等待文件打开完毕后再调用 read 自己,相当于把 read 方法做了一个延迟

文件打开后会调用 open,这时候会再去调用 read,这时候 fd 已经有了,可以进行文件读取

每次读取的文件放到 buffer 当中,并将其抛出

如果是流动模式 就接着下一次读取,直到读取完毕,触发 end/close 事件

可写流

  • fs.createWriteStream("./b.txt")
  • ws.on('open')
  • ws.write
  • ws.on('drain')
  • ws.end
const fs = require('fs');
const ws = fs.createWriteStream('./b.txt', {
  flags: 'w',
  encoding: 'utf8',
  autoClose: true,
  start: 0,
  highWaterMark: 3, // 可写流的highWaterMark和可读流的不一样,表示的是期望用多少内存来写
});

ws.on('open', (fd)=> {
    console.log('open', fd);
})


let flag = ws.write('1');
flag = ws.write('2');
flag = ws.write('345');
ws.end(); // 相当于write+end
// 由于write方法是异步的,所以如果多个write方法同时操作一个文件,就会有出错的情况,
// 解决方式:除了第一次的write,将其他的排队,放入栈中,第一个完成,清空缓存区,
// 如果缓存区过大会导致浪费内存 ,所以会设置一个预期的值,来进行控制,达到预期 后就不要再调用write方法了


let index = 0;
function write(){ // 会浪费内存,只有第一次是真实的写入,后续的都是放入缓存区
    while(index !=5 ){
        ws.write(index++ + '')
    }
}
write();


let index = 0;
function write(){
    let flag = true;
    while(index !=5 && flag){
        flag = ws.write(index++ + '')
    }
}
write();

ws.on('drain', function(){ // 干涸。 当数据达到预期后,并且数据全部被清空写入到文件中,才会触发
    write();
    console.log('drain');
})

setTimeout(()=>{
    ws.end('可以写入内容'); // 表示写入完成,可以放入一些最终的内容并且关闭掉文件 相当于ws.write()+fs.close
})

可写流的实现

const fs = require('fs');
const EventEmitter = require('events');
class WriteStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.flags = options.flags || 'w';
    this.encoding = options.encoding || null;
    this.autoClose = options.autoClose || true;
    this.emitClose = options.emitClose || true;
    this.start = options.start || 0;
    this.highWaterMark = options.highWaterMark || 16 * 1024;

    this.open();

    // 自定义写入流的属性
    this.len = 0; // 记录写入的总个数 如果写入后 会减去写入的个数
    this.offset = this.start; // 写入的偏移量
    this.cache=[]; //缓存写入的操作
    this.needDrain = false; // 是否要触发drain事件
    this.writing = false; //表示是否正在写入 如果正在写入就放到缓存区中
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        return this.destroy(err);
      }
      this.fd = fd; // 供read方法使用
      this.emit('open', fd); // 触发open事件,这时候文件打开完成,可以拿到fd
    });
  }
  write(chunk, encoding=null, cb=() => {}) {
    chunk = Buffer.isBuffer(chunk)? chunk : Buffer.from(chunk);
    this.len += chunk.length;
    this.needDrain = this.len >= this.highWaterMark;

    let oldCb = cb;
     // 对成功的回调进行包装 在每次写入成功之后 先调用成功地回调 再从缓存区取出一个继续写入
     //直到清空缓存区
    cb = () => {
        oldCb();
        this.clearBuffer();
    }

    if(this.writing) {
        // 放到缓存区
        this.cache.push({chunk, encoding, cb});
    }else{
        // 写入文件
        this.writing = true;
        this._write(chunk, encoding, cb);
    }
    return !this.needDrain;
  }
  _write(chunk, encoding, cb) {
      if(typeof this.fd !== 'number') {
          return this.once('open', () => this._write(chunk, encoding, cb));
      }
      fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err,writen) => {
          this.offset += writen;
          this.len -= writen;
          cb();
      })
  }
  clearBuffer() {
      // 写入完毕后 清空缓存区
      let obj = this.cache.shift();// 底层用链表来优化队列
      if(obj) {
        this._write(obj.chunk, obj.encoding, obj.cb);
      }else{
          this.writing = false;
          if(this.needDrain) {
              this.needDrain = false;
              this.emit("drain");
          }
      }
  }
 
}
module.exports = WriteStream;

pipe

  • readSteam.pipe(writeStream);
  • 这个方法是异步的,读取一点写入一点, 可支持大文件的操作
const fs = reuqire("fs");

const rs = fs.createReadStream("1.txt", {highWaterMark: 3});
const ws = fs.createWriteStream("2.txt", {highWaterMark: 1});

rs.on("data", function(chunk) {
    console.log(chunk);
    let flag = ws.write(chunk);
    if(!flag) {
        rs.pause();
    }
})

ws.on("drain", function() {
    console.log("drain");
    rs.resume();
})

// 实现上面功能的简单做法
rs.pipe(ws);
// 所以可读流ReadStream里有个方法叫做pipe,实现原理就是上面的代码

链表

  • 实现单向链表的增删改查功能
  • 反转一个单向链表
class Node{
    constructor(element, next) {
        this.element = element;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head =  null;
        this.size  =  0;  // 链表的总长度
    }
    _getNode(index) {
        let head = this.head;
        for(let i = 0; i <= index; i++) {
            head = head.next;
        }
        return head;
    }
    // 增
    add(index, element) { // 可以传递索引 可以不传索引
        if(arguments.length === 1) {
            element = index;
            index =  this.size;
        }
        if(index == 0) { // 修改链表的头
            let oldHead = this.head;
            this.head = new Node(element, oldHead);
        }else {
            let preNode = this._getNode(index - 1); // 永远找的都是当前索引的前一个节点
            preNode.next = new Node(element, preNode.next);
        }
        this.size++;
    }
    // 删
    remove(index) {
        let removeNode;
        if(index == 0) {
            removeNode = this.head;
            if(removeNode == null) return;
            this.head = this.head.next;
        }else{
            let preNode = this._getNode(index - 1);
            if(!preNode) return;
            removeNode = preNode.next;
            preNode.next = preNode.next.next;
        }
        this.size --;
        return removeNode;
    }
    // 改
    update(index, element) {
        let node = this._getNode(index);
        node.element = element;
    }
    // 查
    getNode(index) {
        return this._getNode(index);
    }
    // 反转单向链表 递归
    reverse() {
        function n(head) {
            if(head == null || head.next == null) return head;
            let newHead = n(head.next);  // 新头变成下一个
            head.next.next = head.next; // 让下一个的next指向老的头
            head.next = null; // 老的头的下一个指向null
            return newHead;
        }
        n(this.head);
    }
    // 反转单向链表 非递归
    reverse(){
        let head = this.head;
        if(head == null || head.next == null) return head;
        let newHead = null;
        while(head) {
            let n = head.next;
            head.next = newHead;
            newHead = head;
            head = n;
        }
        return newHead;
    }
}

let ll = new LinkedList();
ll.add(0,1);
ll.add(0,2);
ll.add(0,3);
console.dir(ll, {depth:1000});
  • 用链表来创建队列
class Queue{
  constructor() {
    this.ll  = new LinkedList();
  }
  add(element) {
    this.ll.add(element)
  }
  peak() {
    return this.ll.remove(0);
  }
}

二叉搜索树

  • 创建二叉搜索树
class Node {
  constructor(element, parent) {
    this.element = element;
    this.parent = perent;
    this.left = null;
    this.right = null;
  }
}

class BST {
  constructor() {
    this.root  =  null;
  }
  add(element) {
    if(this.root == null) {
      this.root = new Node(element, null);
      return;
    }else{
      let current = this.root;
      let parent = null;
      let compare = true;
      while(current) {
        parent = current;
        compare = current.element - element > 0;
        if(compare) {
          current = current.left;
        }else{
          current = current.right;
        }
      }
      let newNode = new Node(element, parent);
      if(compare) {
        parent.left = newNode;
      }else{
        parent.right = newNode;
      }
    }
  }
}
  • 更通用的比较方法
  • 树的遍历
    • 先序遍历 -- 先访问根节点 再左子树 再右子树
      • 遍历一棵树,遇到节点就处理一个节点就可以采用先序遍历
    • 中序遍历 -- 左子树 根节点 右子树 可以用中序遍历的方式升序或者降序处理这棵树
      • 按顺序处理
      • 按照二叉搜索树的特性:遍历的结果是升序的
    • 后序遍历 -- 左子树 右子树 根节点
      • 每次都是先处理儿子再处理自己
    • 层序遍历
    • 广度优先遍历
    • 深度优先遍历
  • 反转二叉树
    • 下面实现的四种遍历都能实现反转二叉树
class Node {
  constructor(element, parent) {
    this.element = element;
    this.parent = perent;
    this.left = null;
    this.right = null;
  }
}

class BST {
  constructor(compare) {
    this.root  =  null;
    let internalCompare = this.compare;
    this.compare = compare || internalCompare;
  }
  compare(current, element) {
    return current - element > 0;
  }
  add(element) {
    if(this.root == null) {
      this.root = new Node(element, null);
      return;
    }else{
      let current = this.root;
      let parent = null;
      let compare = true;
      while(current) {
        parent = current;
        compare = this.compare(current.element, element);
        if(compare) {
          current = current.left;
        }else{
          current = current.right;
        }
      }
      let newNode = new Node(element, parent);
      if(compare) {
        parent.left = newNode;
      }else{
        parent.right = newNode;
      }
    }
  }
  // 先序遍历
  preorderTraversal(cb){
    const traversal = node => {
      if(node == null) return;
      cb(node);
      traversal(node.left);
      traversal(node.right);
    }
    traversal(this.root);
  }
  // 中序遍历
  inorderTraversal(cb){
    const traversal = node => {
      if(node == null) return;
      traversal(node.left);
      cb(node);
      traversal(node.right);
    }
    traversal(this.root);
  }
  // 后序遍历
  postorderTraversal(cb){
    const traversal = node => {
      if(node == null) return;
      traversal(node.left);
      traversal(node.right);
      cb(node);
    }
    traversal(this.root);
  }
  // 层序遍历
  levelorderTraversal(cb) {
    let stack =  [this.root];
    let i  = 0;
    let currentNode;
    while(currentNode = stack[i++]) {
      cb(currentNode);
      if(currentNode.left) {
        stack.push(current.left);
      }
      if(currentNode.right) {
        stack.push(currentNode.right);
      }
    }
  }
  // 反转二叉树
  invertTree(cb) {
    const traversal = (node) => {
      if(node == null) return;

      let r =  node.right;
      node.right = nodee.left;
      nodee.left = r;

      traversal(node.left);
      traversal(node.right);
      cb(node);
    }
    traversal(this.root)
    return this.root;
  }
}

let bst = new BST((a,b) => {
  return a.age - b.age > 0
});
let arr = [{age: 15}, {age: 18}, {age: 9}, {age: 12}, {age: 23}];
arr.forEach(item =>  {
  bst.add(item);
})

bst.preorderTraversal((node) => {
  console.log(node.element)
})