编码
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 的三种方式
let buf1 = Buffer.alloc(6);
let buf2 = Buffer.from('哈哈');
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
截取想要的 bufferbuffer.copy
拷贝 bufferbuffer.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")
})
读取文件
- 去读文件 文件必须存在 不能通过/读取内容, /表示的是根目录
- 读取的默认类型是 buffer
let result = fs.readFileSync('log.js');
// 两种方式转result的格式:
result.toString();
fs.readFileSync('log.js', 'utf8');
写入文件
- 读取类型都是 buffer, 写入的时候都是 utf8
- 读的文件必须存在, 写的时候文件不存在会自动创建,里面的内容会被覆盖掉
- 会对第二个参数(文件内容)默认调用 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)
})