一、进制转换
1. 二进制转化为十进制
1001 => 1*2^3 + 0*2^2 + 0*2^1 + 1*2^0 = 9
parseInt('1001', 2); => 9
2. 十进制转化为二进制
除2取余
3. 任意进制转化为任意进制
# 0b 二进制 0o 八进制 0x 十六进制
(0x64).toString(2) => '1100100';
4. 0.1+0.2为啥不等于0.3
十进制中的0.5,二进制是多少
10 => 0.5 差20倍
2 => 0.1
小数转化为二进制怎么转?
乘二取整
0.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.2 * 2 ...
0.1转化为二进制可以看到无限循环了,IEEE754标准中,8个字节也就是64位表示一个数字,超出以后,会将超出部分截取掉,这就造成了精度缺失。
开发中怎么解决这种问题呢?
parseFloat(0.1+0.2).toFixed(10) // "0.3000000000"
二、Buffer
Node中,用Buffer来标识内存的数据。node把内容转换成16进制来显示(16进制比较短,16进制最大0xff即最大是255)。
Node只支持UTF8编码格式。utf8一个汉字3个字节,GBK一个汉字两个字节。
Buffer代表的是内存,内存是一段固定的空间,阐述的内存是固定大小,不能随意添加。因此有了扩容的概念,动态创建一个新的内存,把内容迁过去。
1.Buffer.alloc() 声明buffer
// 开辟5个字节的内存
let buffer = Buffer.alloc(5) // <buffer 00 00 00 00 00>
2.Buffer.from() 字符串转buffer
// 指定每个字节的数值(超过255会取余)
let buffer = Buffer.from([1, 2, 3, 4, 5]); // <Buffer 01 02 03 04 05>
let buffer = Buffer.from([0x25, 0x26, 0x64]) // <buffer 25 26 64>
// 每个汉字,3个字节
let buffer = Buffer.from('天师'); // <Buffer e5 a4 a9 e5 b8 88>
3.Buffer.toString() 转化为指定编码
// 转化为base64
let buffer = Buffer.from('师').toString('base64'); // 5biI
// 转化为utf8
let buffer2 = Buffer.from('师').toString('utf8'); // 师
// 不传参默认是utf8
let buffer2 = Buffer.from('师').toString(); // 师
4.Buffer.slice 截取buffer
let buffer = Buffer.from([1, 2, 3, 4, 5]); // <Buffer 01 02 03 04 05>
let sliceBuffer = buffer.slice(0, 1);
sliceBuffer[0] = 100;
console.log(buffer); // <Buffer 64 02 03 04 05>
5.Buffer.copy buffer拷贝
let buf1 = Buffer.from('小小');
let buf2 = Buffer.from('天');
let buf3 = Buffer.from('师');
let bigBuffer = Buffer.alloc(12);
buf1.copy(bigBuffer, 6, 0, 6); // 跳过6个字节后,从第0个开始拷贝6个字节
buf2.copy(bigBuffer, 0, 0, 3);
buf3.copy(bigBuffer, 3); // 默认全拷贝进去
console.log(bigBuffer.toString()); // 天师小小
6.Buffer.concat buffer拼接
let buf1 = Buffer.from('小小');
let buf2 = Buffer.from('天');
let buf3 = Buffer.from('师');
let bigBuffer = Buffer.concat([buf1, buf2, buf3], 6).toString();
console.log(bigBuffer); // 小小
let bigBuffer = Buffer.concat([buf1, buf2, buf3]).toString();
console.log(bigBuffer); // 小小天师
7.Buffer.isBuffer
Buffer.isBuffer(bigBuf);
8.buffer.length
9.base64编码
base64编码过程:
let buffer = Buffer.from('师'); // <Buffer e5 b8 88>
let base64 = buffer.toString('base64'); // 5biI
// 将每个字节转化为二进制
console.log(0xe5.toString(2)); // 11100101
console.log(0xb8.toString(2)); // 10111000
console.log(0x88.toString(2)); // 10001000
// 11100101 10111000 10001000 // 8位 x 3字节 => 6位 x 4字节 // 多了三分之一字节
// 111001 011011 100010 001000
// 转化成十进制
console.log(parseInt('111001', 2)); // 57
console.log(parseInt('011011', 2)); // 27
console.log(parseInt('100010', 2)); // 34
console.log(parseInt('001000', 2)); // 8
let str = 'ABCDEFGHIJKLMNOPKRSTUVWXYZ';
str += str.toLocaleLowerCase();
str += '0123456789+/';
console.log(str[57] + str[27] + str[34] + str[8]); // 5biI
base64可以将字符串放到任意路径的链接里,但是文件体积会变大,base64转化完毕后会比之前大1/3。
三、文件操作
1. 文件读写
const fs = require('fs');
const path = require('path');
fs.readFile(path.resolve(__dirname, 'index.html'), (err, data) => {
fs.writeFile(path.resolve(__dirname, './test.html'), data, (err, data) => {
console.log(data);
})
})
这种方式有个问题:必须全部读取完成后才可以写入,读取到的内容放到内存里,会造成内存淹没。
怎么做到边读边写呢?使用read和write可以做到。
function copy(source, target, cb) {
const BUFFER_SIZE = 3; // 每次读写3个字节
const buffer = Buffer.alloc(BUFFER_SIZE);
let r_offset = 0; // 偏移量
let w_offset = 0;
fs.open(source, 'r', (err, rfd) => { // rfd是打开文件的文件描述符
fs.open(target, 'w', (err, wfd) => {
function next() {
fs.read(rfd, buffer, 0, BUFFER_SIZE, r_offset, (err, bytesRead) => {
if (err) return cb(err);
if (bytesRead) {
fs.write(wfd, buffer, 0, bytesRead, w_offset, (err, bytesWrite) => {
r_offset += bytesRead;
w_offset += bytesWrite;
});
next();
} else {
fs.close(rfd, () => {});
fs.close(wfd, () => {});
cb();
}
})
}
next();
})
})
}
copy('./a.txt', './b.txt', (err) => {
if (err) return console.log('err', err);
console.log('copy success');
})
功能实现了,但是读写不分离,我们需要把读和写拆分开,怎么做呢?发布订阅实现读写分离。
四、可读流
可读流,可以控制读取的个数和读取的速率,可以实现边读编写,且读写分离。
fs模块基于stream流实现了文件读写流。fs.createReadStream用来读文件。
// 使用ReadStream
const fs = require('fs');
const ReadStream = require('./ReadStream.js');
// let rs = createReadStream('./a.txt', {
let rs = new ReadStream('./a.txt', {
flags: 'r',
encoding: null, // 编码是buffer类型
autoClose: true, // 读取完成后自动关闭
start: 0, // 从哪儿开始读
// end: 1, // 包前包后,应该输出两个
highWaterMark: 3 // 每次读取的数据个数,默认是64*1024字节
});
rs.on('open', function(fd) {
console.log(fd);
})
rs.on('data', function(chunk) {
console.log(chunk);
// rs.pause(); // 暂停读取
})
rs.on('end', function () {
console.log('end');
})
rs.on('close', function () {
console.log('close');
})
rs.on('error', function (err) {
console.log('err', err);
})
// setInterval(() => {
// rs.resume(); // 恢复读取
// }, 1000)
1. read拿到文件标识符fd,接下来可以进行读取操作
// ReadStream.js
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;
// open之后要开始read,open是异步的
this.open();
// 用户监听了data事件,才需要读取
// EventEmitter底层实现,绑定了监听事件,就会触发newListener
this.on('newListener', function(type) { // type => open data end close error
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);
})
}
read() {
// 为了拿到fd,多做了一次检测。
// open是移步的,read是同步执行,直接拿的话是拿不到的。
if (typeof this.fd !== 'number') {
return this.once('open', () => this.read())
}
console.log('拿到', this.fd);
}
destroy(err) {
if (err) this.emit('error', err);
}
}
module.exports = ReadStream;
2. 文件读取
如果配置了end,则根据end截取:
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;
// open之后要开始read,open是异步的
this.open();
// 用户监听了data事件,才需要读取
// EventEmitter底层实现,绑定了监听事件,就会触发newListener
this.on('newListener', function(type) { // type => open data end close error
if (type === 'data') {
this.read();
}
+ this.offset = this.start;
})
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) return this.destroy(err);
this.fd = fd; // 文件标识绑定到实例上,read要用它
this.emit('open', fd);
})
}
read() {
// 为了拿到fd,多做了一次检测。
// open是移步的,read是同步执行,直接拿的话是拿不到的。
if (typeof this.fd !== 'number') {
return this.once('open', () => this.read())
}
// console.log(this.fd);
// 如果配置了end,则按照end截取
+ let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
+ const buffer = Buffer.alloc(howMatchToRead);
// 读取文件,buffer类型,从第0个开始读,读取多少个,偏移量
+ fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
// 读取到数据
+ if (bytesRead) {
+ this.offset += bytesRead;
+ this.emit('data', buffer.slice(0, bytesRead));
+ 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'));
+ }
}
}
module.exports = ReadStream;
3. 暂停、继续读取
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; // 是否递归读取
// open之后要开始read,open是异步的
this.open();
// 用户监听了data事件,才需要读取
// EventEmitter底层实现,绑定了监听事件,就会触发newListener
this.on('newListener', function(type) { // type => open data end close error
if (type === 'data') {
+ this.flowing = true;
this.read();
}
this.offset = this.start;
})
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) return this.destroy(err);
this.fd = fd; // 文件标识绑定到实例上,read要用它
this.emit('open', fd);
})
}
read() {
// 为了拿到fd,多做了一次检测。
// open是移步的,read是同步执行,直接拿的话是拿不到的。
if (typeof this.fd !== 'number') {
return this.once('open', () => this.read())
}
// console.log(this.fd);
// 如果配置了end,则按照end截取
let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
const buffer = Buffer.alloc(howMatchToRead);
fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
if (bytesRead) {
this.offset += bytesRead;
this.emit('data', buffer);
+ if (this.flowing) this.read();
} else{
this.emit('end');
this.destroy();
}
})
}
+ resume() {
+ if (!this.flowing) {
+ this.flowing = true;
+ this.read();
+ }
+ }
+ pause() {
+ this.flowing = false;
+ }
destroy(err) {
if (err) this.emit('error', err);
if (this.autoClose) {
fs.close(this.fd, () => this.emit('close'))
}
}
}
module.exports = ReadStream;
4. 完整可读流
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;
// open之后要开始read,open是异步的
this.open();
// 用户监听了data事件,才需要读取
// EventEmitter底层实现,绑定了监听事件,就会触发newListener
this.on('newListener', function(type) { // type => open data end close error
if (type === 'data') {
this.flowing = true;
this.read();
}
this.offset = this.start;
})
}
// 监听可读流的触发事件,实现读一点,写一点
+ pipe(ws) {
+ this.on('data', (data) => {
+ let flag = ws.write(data);
+ if (!flag) {
+ this.pause();
+ }
+ })
+ ws.on('drain', () => {
+ this.resume();
+ })
+ }
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) return this.destroy(err);
this.fd = fd; // 文件标识绑定到实例上,read要用它
this.emit('open', fd);
})
}
read() {
// 为了拿到fd,多做了一次检测。
// open是移步的,read是同步执行,直接拿的话是拿不到的。
if (typeof this.fd !== 'number') {
return this.once('open', () => this.read())
}
// console.log(this.fd);
// 如果配置了end,则按照end截取
let howMatchToRead = this.end ? Math.min((this.end - this.offset + 1), this.highWaterMark) : this.highWaterMark;
const buffer = Buffer.alloc(howMatchToRead);
fs.read(this.fd, buffer, 0, howMatchToRead, this.offset, (err, bytesRead) => {
if (bytesRead) {
this.offset += bytesRead;
this.emit('data', buffer);
if (this.flowing) this.read();
} else{
this.emit('end');
this.destroy();
}
})
}
resume() {
if (!this.flowing) {
this.flowing = true;
this.read();
}
}
pause() {
this.flowing = false;
}
destroy(err) {
if (err) this.emit('error', err);
if (this.autoClose) {
fs.close(this.fd, () => this.emit('close'))
}
}
}
module.exports = ReadStream;
五、可写流
let ws = fs.createWriteStream('./b.txt', {
flags: 'w',
encoding: 'utf-8',
autoClose: true,
start: 0,
highWaterMark: 3, // 期望用多少内存来写,和文件读取的含义不同
})
ws.on('open', (fd) => {
console.log('open', fd);
})
ws.on('close', () => {
console.log('close');
})
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
ws.end();
- write方法是并发异步的,如果多个write方法同时操作一个文件,就会有出错的情况。
- 解决办法是:第一次允许直接写入文件,在第一次没有写入完成前,再有写入操作,则放入内存队列中。等到第一次写入完毕,将并发异步操作改为串行异步,一个一个从内存队列中拿出来执行写入操作。
- 写入超过highWaterMark预期的情况下,flag返回false,不超过返回true。
这种解法会额外占用内存,我们希望的是读一点,写一点,再没有写入完成前,暂停读取,写入完成后继续读取。如何做到,见第3小节。
1. 实现可写流
const fs = require('fs');
const EventEmitter = require('events');
// const Queue = require('./queue.js');
class WriteStream extends EventEmitter {
constructor(path, options) {
super();
this.path = path;
this.flags = options.flags || 'w';
this.encoding = options.encoding || 'utf8';
this.mode = options.mode || 0o666;
this.autoClose = options.autoClose || true;
this.start = options.start || 0;
this.highWaterMark = options.highWaterMark || 16 * 1024;
this.len = 0; // 待写入文件的数据长度
this.needDrain = false; // 需要触发drain
this.cache = [];
// this.cache = new Queue;
this.writing = false; // 是不是正在写入
this.offset = this.start; // 偏移量
this.open();
}
open() {
fs.open(this.path, this.flags, this.mode, (err, fd) => {
this.fd = fd;
this.emit('open', fd);
})
}
write(chunk, encoding = this.encoding, cb = () => {}) {
// 将数据转化为buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.len += chunk.length;
// 是否达到预期
let returnValue = this.len < this.highWaterMark;
this.needDrain = !returnValue;
// AOP增加写入成功后清空缓存的逻辑
let userCb = cb;
cb = () => {
userCb();
this.clearBuffer();
}
// 判断是不是正在写入,如果为false,表示是第一次写,后面的都先放到cache缓存区
if (!this.writing) {
// 直接写入
this.writing = true;
this._write(chunk, encoding, cb);
} else {
// 保存到缓存区
// this.cache.offer({
this.cache.push({
chunk,
encoding,
cb
})
}
return returnValue;
}
_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, written) => {
this.offset += written; // 维护偏移量
this.len -= written; // 减少缓存的个数
cb(); // 写入成功回调
})
}
// 清空缓存区
clearBuffer() {
let data = this.cache.shift();
// let data = this.cache.poll();
// 继续写入
if (data) {
this._write(data.chunk, data.encoding, data.cb);
// 缓存区都清空后
} else {
this.writing = false;
if (this.needDrain) {
this.needDrain = false;
this.emit('drain');
}
}
}
}
module.exports = WriteStream;
数组的好处是可以通过索引直接拿到某个元素。但是如果删除数组中间的元素,数组后面的元素都需要做移动,这是比较耗性能的。
链表的优势是头尾操作,但如果中间插入元素,链表也需要从头遍历,找要插入的位置,因此中间插入这种情况,链表相对于数组优势不明显。
可写流这种情形,正是操作的头尾,因此用链表性能更好。
// queue.js 用链表实现的队列
const LinkedList = require('./linkList.js');
class Queue {
constructor() {
this.ll = new LinkedList;
}
poll() {
let removeNode = this.ll.remove(0);
return removeNode && removeNode.element;
}
offer(element) {
this.ll.add(element);
}
}
module.exports = Queue;
// linkList.js 链表本表
class Node {
constructor(element, next) {
this.element = element;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0; // 链表长度
}
add(index, element) {
// 如果只传了一个参数,统一成两个的形式
if (arguments.length === 1) {
element = index;
index = this.size;
}
// 越界判断
if (index < 0 || index > this.size) throw new Error('越界');
// 第一个节点
if (index === 0) {
let head = this.head;
this.head = new Node(element, head);
// 不是第一个节点,修改next指针
} else {
let prevNode = this._node(index - 1); // 找前一个节点,索引是size-1
prevNode.next = new Node(element, prevNode.next);
}
this.size++;
}
remove(index) {
let removeNode;
// 只有一个的情况,删除的是头
if (index === 0) {
removeNode = this.head;
if (removeNode) {
this.head = this.head.next;
this.size--;
}
// 有多个,则把上一个的指针指向以上一个为基础的,下一个的下一个
} else {
let prevNode = this._node(index - 1);
removeNode = prevNode.next;
prevNode.next = prevNode.next.next;
this.size--;
}
return removeNode;
}
// 查找节点
_node(index) {
let current = this.head;
for (let i = 0; i < index; i++) {
current = current.next;
}
return current;
}
}
let link = new LinkedList();
link.add(1);
link.add(2);
link.add(3);
// link.add(1, 20); // 根据索引添加
// link.remove(1);
// console.dir(link, { depth: 100 });
module.exports = LinkedList;
六、边读边写
边读边写节约内存,利用可写流的pause和resume, 可以实现边读边写。
const rs = fs.createReadStream('./a.txt', {
highWaterMark: 3 // 读取默认64k
});
const ws = fs.createWriteStream('./b.txt', {
highWaterMark: 2 // 写入默认16k
})
// 读取到的数据
rs.on('data', (data) => {
let flag = ws.write(data);
if (!flag) {
console.log('吃不下了,正在消化');
rs.pause(); // 暂停往文件里写
}
})
// 数据写入完毕
ws.on('drain', (data) => {
console.log('消化完了,继续喂我');
rs.resume(); // 继续往文件里写
})
const ReadStream = require('./ReadStream.js');
const WriteStream = require('./WriteStream.js');
const rs = new ReadStream('./a.txt', {
highWaterMark: 2 // 读取默认64k
});
const ws = new WriteStream('./b.txt', {
highWaterMark: 1 // 写入默认16k
})
rs.pipe(ws);