目录
二、核心模块
21、 Nodejs事件环理解
setTimeout(() => {
console.log('s1');
Promise.resolve().then(() => {
console.log('p1');
})
Promise.resolve().then(() => {
console.log('t1');
})
});
Promise.resolve().then(() => {
console.log('p2');
})
console.log('start');
setTimeout(() => {
console.log('s2');
Promise.resolve().then(() => {
console.log('p3');
})
Promise.resolve().then(() => {
console.log('t2');
})
});
console.log('end');
// start end p2 s1 p1 t1 s2 p3 t2
22、 Nodejs与浏览器事件环区别
(1)任务队列数不同
- 浏览器中只有二个任务队列
- Nodejs 中有6个事件队列
(2)Nodejs 微任务执行实际不同
- 二者都会在同步代码执行完毕后执行微任务
- 浏览器平台下每当一个宏任务执行完毕后就清空微任务
- Nodejs 平台在事件队列切换时会去清空微任务
(3)微任务优先级不同
- 浏览器事件环中,微任务存放于事件队列,先进先出
- Nodejs 中 process.nextTick 先于 promise.then
23、Nodejs事件环常见问题
(1)setTimeout 和 setImmediate同步执行顺序是随机的
setTimeout(() => {
console.log('timeout');
});
setImmediate(() => {
console.log('immediate');
})
// 会出现两种结果
// 第一种
// timeout
// immediate
// 第二种
// immediate
// timeout
原因:是因为 setTimeout后面要跟一个时间,没写的情况下默认是0,不管是在Node和浏览器的平台下,都会有一个不稳定的因素,有些情况下会产生一些延时,延时的话,代码从上往下,会先加载 setImmediate,后加载 setTimeout;没有延时,就是先加载 setTimeout,后加载 setImmediate
解决办法:放在 I/O 回调函数,就不会出现这样的情况
const fs = require('fs')
fs.readFile('./m1.js', () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
})
})
// immediate
// timeout
24、核心模块之stream
(1) 将左边的数据写到右边 js 文件中
(2)Node.js 诞生之初就是为了提高 IO 性能
(3)文件操作系统和网络模块实现了流接口
(4)Node.js 中的流就是处理流式数据的抽象接口
(5)应用程序中为什么使用流来处理数据?
原来方式常见问题:
- 同步读取资源文件,用户需要等待数据读取完成
- 资源文件最终一次性加载至内存,开销较大
现在方式
流处理数据的优势
- 时间效率:流的分段处理可以同时操作多个数据 chunk
- 空间效率:同一时间流无须占据大内存空间
- 使用方便:流配合管理,扩展程序变得简单,
(6)Node.js 内置了 stream,它实现了流操作对象
(7)Node.js 中流的分类
- Readable:可读流,能够实现数据的读取
- Writeable:可写流,能够实现数据的写操作
- Duplex:双工流,即可读又可写
- Tranform:转换流,可读可写,还能实现数据转换
(8)Node.js 流特点
- Stream 模块实现了四个具体的抽象
- 所有流都继承自 EventEmitter
25、stream之可读流
(1)生产供程序消费数据的流
(2)如何自定义可读流?
- 继承 stream 里的 Readable
- 重写 _read 方法调用 push 产生数据
const { Readable } = require('stream')
// 模拟底层数据
let source = ['lg', 'zce', 'syy']
// 自定义类继承 Readable
class MyReadable extends Readable {
constructor(source) {
super()
this.source = source
}
_read() {
let data = this.source.shift() || null
this.push(data)
}
}
// 实例化
let myReadable = new MyReadable(source)
// myReadable.on('readable', () => {
// let data = null
// while((data = myReadable.read(2)) !== null) {
// console.log(data.toString());
// // lg
// // zc
// // es
// // yy
// }
// })
myReadable.on('data', (data) => {
console.log(data.toString());
// lg
// zce
// syy
})
(3)自定义可读流问题
- 底层数据读取完成之后如何处理?
- 数据传递完之后,传入一个 null
- 消费者如何获取可读流中的数据?
- readable 事件:当流中存在可读取数据时触发
- data 事件:当流中数据块传给消费者厚触发
- 消费数据为什么存在二种方式?
- 满足不同的使用场景
- 流动模式
- 暂停模式
- 两者区别在于消费数据是否需要主动调用 readable 来读取数据
(4)可读流总结
- 明确数据生产与消费流程
- 利用 API 实现自定义的可读流
- 明确数据消费的事件使用
26、stream之可写流
(1)用于消费数据的流
(2)自定义可写流
- 继承 stream 模块的 Writeable
- 重写 _write 方法,调用 write 执行写入
(3)可写流事件
- pipe 事件:可读流调用 pipe() 方法时触发
- unpipe 事件::可读流调用 unpipe 方法时触发
const { Writable } = require('stream')
class MyWriteable extends Writable {
constructor() {
super()
}
_write(chunk, en, done) {
process.stdout.write(chunk.toString() + '<----')
process.nextTick(done)
}
}
let myWriteable = new MyWriteable()
myWriteable.write('拉钩教育', 'utf-8', () => {
console.log('end');
})
// 拉钩教育<----end
27、stream之双工和转换流
(1)可读、可写、双工、转换是单一抽象具体实现
(2)Node.js 诞生的初衷就是解决密集型 IO 事务
(3)Node.js 中处理数据模块继承了流和 EventEmitter
(4)stream、四种类型流、实现流操作的模块
(5)Duplex 是双工流,技能生产又能消费
(6)自定义双工流
- 继承 Duplex
- 重写 _read 方法,调用 push 生产数据
- 重写 _write 方法,调用 write 消费数据
let { Duplex } = require('stream')
class MyDuplex extends Duplex {
constructor(source) {
super()
this.source = source
}
_read() {
let data = this.source.shift() || null
this.push(data)
}
_write(chunk, en, next) {
process.stdout.write(chunk)
process.nextTick(next)
}
}
let source = ['a', 'b', 'c']
let myDuplex = new MyDuplex(source)
// myDuplex.on('data', (chunk) => {
// console.log(chunk.toString());
// })
// a
// b
// c
myDuplex.write('拉钩教育', () => {
console.log(111);
})
// 拉钩教育111
(7)Transform 也是一个双工流
它和 Duplex 的区别是,Duplex的读和写是独立的
自定义转换流
- 继承 Transform 类
- 重写 _transfrm 方法,调用 push 和 callback
- 重写 _flush 方法,处理剩余数据
let { Transform } = require('stream')
class MyTransform extends Transform {
constructor() {
super()
}
_transform(chunk, en, cb) {
this.push(chunk.toString().toUpperCase())
cb(null)
}
}
let t = new MyTransform()
t.write('a')
t.on('data', (chunk) => {
console.log(chunk.toString());
})
// A
(8)Node.js 中的四种流
- Readable 可读流
- Writeable 可写流
- Duplex 双工流
- Trnsform 转换流
28、 文件可读流创建和消费
const fs = require('fs')
let rs = fs.createReadStream('text.txt', {
flags: 'r', // 文件流以什么样的方式打开或者操作 r 表示 read 可读
encoding: null, // 对编码进行设置,默认 null 返回的是 Buffer 数据类型
fd: null, // 文件标识符,默认值从3开始的,0,1,2,是标准输入、输出和错误占用
mode: 438, // 权限类,十进制是438,八进制是0O66
autoClose: true, // 是否是自动关闭文件
start: 0, // 当前从底层的哪个位置读取,默认是 0
// end: 3, // 到哪个位置结束读取
highWaterMark: 2 // 水位线,表示每次要读取多少个字节的数据
})
// rs.on('data', (chunk) => {
// console.log(chunk.toString());
// rs.pause()
// setTimeout(() => {
// rs.resume()
// }, 1000)
// })
// 01 23 45 67 89
rs.on('readable', () => {
// let data = rs.read()
// console.log(data);
// <Buffer 30 31>
// <Buffer 32 33>
// <Buffer 34 35>
// <Buffer 36 37>
// <Buffer 38 39>
// let data
// while((data = rs.read()) !== null) {
// console.log(data.toString());
// // 01 23 45 67 89
// }
let data
while((data = rs.read(1)) !== null) {
console.log(data.toString());
console.log('------', rs._readableState.length);
// 0
// ------ 1
// 1
// ------ 0
// 2
// ------ 1
// 3
// ------ 0
// 4
// ------ 1
// 5
// ------ 0
// 6
// ------ 1
// 7
// ------ 0
// 8
// ------ 1
// 9
// ------ 0
}
})
29、文件可读流事件与应用
const fs = require('fs')
let rs = fs.createReadStream('text.txt', {
flags: 'r', // 文件流以什么样的方式打开或者操作 r 表示 read 可读
encoding: null, // 对编码进行设置,默认 null 返回的是 Buffer 数据类型
fd: null, // 文件标识符,默认值从3开始的,0,1,2,是标准输入、输出和错误占用
mode: 438, // 权限类,十进制是438,八进制是0O66
autoClose: true, // 是否是自动关闭文件
start: 0, // 当前从底层的哪个位置读取,默认是 0
// end: 3, // 到哪个位置结束读取
highWaterMark: 2 // 水位线,表示每次要读取多少个字节的数据
})
rs.on('open', (fd) => {
console.log(fd, '文件打开了');
})
rs.on('close', () => {
console.log('文件关闭了');
})
let bufferArr = []
rs.on('data', (chunk) => {
bufferArr.push(chunk)
})
rs.on('end', () => {
console.log(Buffer.concat(bufferArr).toString())
console.log('当数据被清空之后');
})
rs.on('error', (err) => {
console.log('出错了');
})
// 3 文件打开了
// 0123456789
// 当数据被清空之后
// 文件关闭了
注意: end 事件在 close 事件之前结束
30、 文件可写流
const fs = require('fs')
const ws = fs.createWriteStream('test.txt', {
flags: 'w',
mode: 438,
fd: null,
encoding: 'utf-8',
start: 0,
highWaterMark: 3
})
let buf = Buffer.from('abc')
// buf 类型是 字符串 或者 buffer ==> fs rs
// ws.write(buf, ()=> {
// console.log('ok2');
// })
// ws.write('拉钩教育', () => {
// console.log('ok1');
// })
ws.on('open', (fd) => {
console.log('open', fd); // open 3
})
ws.write('2')
// close 是在数据写入操作全部完成之后再执行
ws.on('close', () => {
console.log('文件关闭了');
})
// end 执行之后就意味着数据写入操作完成
ws.end('拉钩教育')
// error
ws.on('error', (err) => {
console.log('出错了');
})
31、write执行流程
const fs = require('fs')
let ws = fs.createWriteStream('test.txt', {
highWaterMark: 3
})
let flag = ws.write('1')
console.log(flag)
// true
flag = ws.write('2')
console.log(flag)
// true
// 如果 flag 为 false 并不是说明当前数据不能被执行写入
flag = ws.write('3')
console.log(flag)
// false
// flag 为 false 会执行drain,否则不执行
ws.on('drain', () => {
console.log('11');
})
// 11
/**
* 01 第一次调用 write 方法时是将数据直接写入到文件中
* 02 第二次开始 write 方法就是将数据写入至缓存中
* 03 生产速度 和 消费速度 是不一样的,一般情况下生产速度要比消费速度快很多
* 04 当 flag 为 false 之后并不意味着当前次的数据不能被写入了,但是我们应该告知数据的生产者,当前的消费速度已经跟不上 生产速度了。所以这个时候,一版我们会将可读流的模块修改为在听模式
* 05 当数据生产者暂停之后,消费者会慢慢的消化它内部缓存中的数据,直接可以再次被执行写入操作
* 06 当缓冲区可以继续写入数据时如何让生产者知道? drain 事件
*/
32、控制写入速度
- 一次性写入
- 分批写入
/**
* 需求: ‘拉钩教育’写入指定的文件
* 01 一次性写入
* 02 分批写入
* 对比
*/
let fs = require('fs')
let ws = fs.createWriteStream('test.txt', {
highWaterMark: 3
})
// 一次性写入
// ws.write('拉钩教育')
// 分批写入
let source = "拉钩教育".split('')
let num = 0
let flag = true
function executeWrite() {
flag = true
while(num !== 4 && flag) {
flag = ws.write(source[num])
num ++
}
}
executeWrite()
ws.on('drain', () => {
console.log('drain 执行了');
executeWrite()
})
// drain 执行了
// drain 执行了
// drain 执行了
// drain 执行了
32、背压机制
(1)Node.js 的 stream 已实现了背压机制
(2)如果不进行数据的背压,就会出现内存溢出、GC频繁调用、其他进程变慢
let fs = require('fs')
let rs = fs.createReadStream('text.txt', {
highWaterMark: 4
})
let ws = fs.createWriteStream('textDemo.txt', {
highWaterMark: 1
})
// let flag = true
// rs.on('data', (chunk) => {
// flag = ws.write(chunk, () => {
// console.log('写完了');
// })
// if (!flag) {
// rs.pause()
// }
// })
// ws.on('drain', () => {
// rs.resume()
// })
// 或者 用 Pipe 方式写
rs.pipe(ws)
33、 模拟文件可读流01
const fs = require('fs')
const EventEmitter = require('events')
class MyFileReadStream extends EventEmitter{
constructor(path, options = {}) {
super()
this.path = path
this.flags = options.flags || "r"
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end
this.highWaterMark = options.highWaterMark || 64 * 1024
this.readOffset = 0
this.open()
this.on('newListener', (type) => {
if (type === 'data') {
this.read()
}
})
}
open() {
// 原生 open 方法来打开指定位置上的文件
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error', err)
}
this.fd = fd
this.emit('open', fd)
})
}
read() {
if (typeof this.fd !== 'number') {
return this.once('open', this.read)
}
let buf = Buffer.alloc(this.highWaterMark)
let howMuchToRead
/* if (this.end) {
howMuchToRead = Math.min(this.end - this.readOffset + 1, this.highWaterMark)
} else {
howMuchToRead = this.highWaterMark
} */
howMuchToRead = this.end ? Math.min(this.end - this.readOffset + 1, this.highWaterMark) : this.highWaterMark
fs.read(this.fd, buf, 0, howMuchToRead, this.readOffset, (err, readBytes) => {
if (readBytes) {
this.readOffset += readBytes
this.emit('data', buf.slice(0, readBytes))
this.read()
} else {
this.emit('end')
this.close()
}
})
}
close() {
fs.close(this.fd, () => {
this.emit('close')
})
}
}
let rs = new MyFileReadStream('test.txt', {
end: 7,
highWaterMark: 3
})
rs.on('data', (chunk) => {
console.log(chunk)
})
34、链表结构
(1)为什么不采用数组存储数据?
- 数组存储数据的长度具有上限
- 数组存在塌陷问题
(2)链表是一系列节点的集合
每个节点都具有指向下一个节点的属性
(3)链表分类
- 双向链表
- 单向链表
- 循环链表
35、 单向链表实现
/**
* 01 node + head + null
* 02 head ---> null
* 03 size
* 04 next element
* 05 增加 删除 修改 查询 清空
*/
class Node {
constructor(element, next) {
this.element = element
this.next = next
}
}
class LinkedList {
constructor(head, size) {
this.head = null
this.size = 0
}
// 获取节点的位置
_getNode(index) {
if (index < 0 || index >= this.size) {
throw new Error('越界了')
}
let currentNode = this.head
for (let i = 0; i < index; i++) {
currentNode = currentNode.next
}
return currentNode
}
// 增加
add(index, element) {
if (arguments.length === 1) {
element = index
index = this.size
}
if (index < 0 || index > this.size) {
throw new Error('cross the border')
}
if (index === 0) {
let head = this.head // 保存原有 head 的指向
this.head = new Node(element, head)
} else {
let prevNode = this._getNode(index - 1)
prevNode.next = new Node(element, prevNode.next)
}
this.size++
}
// 删除
remove(index) {
if (index === 0) {
let head = this.head
this.head = head.next
} else {
let prevNode = this._getNode(index -1)
prevNode.next = prevNode.next.next
}
this.size--
}
// 修改
set(index, element) {
let node = this._getNode(index)
node.element = element
}
// 查询
get(index) {
return this._getNode(index)
}
// 清空
clear() {
this.head = null
this.size = 0
}
}
const l1 = new LinkedList()
l1.add('node1')
l1.add('node2')
l1.add(1,'node3')
// console.log(l1);
// LinkedList { head: Node { element: 'node1', next: null }, size: 1 }
// l1.remove(1)
// console.log(l1);
// LinkedList {
// head: Node {
// element: 'node1',
// next: Node { element: 'node2', next: null }
// },
// size: 2
// }
l1.set(1, 'node-3-3')
// console.log(l1);
// LinkedList {
// head: Node {
// element: 'node1',
// next: Node { element: 'node-3-3', next: [Node] }
// },
// size: 3
// }
let a = l1.get(0)
// console.log(a);
// Node {
// element: 'node1',
// next: Node {
// element: 'node-3-3',
// next: Node { element: 'node2', next: null }
// }
// }
l1.clear()
console.log(l1);
// LinkedList { head: null, size: 0 }
36、单向链表实现队列
/**
* 01 node + head + null
* 02 head ---> null
* 03 size
* 04 next element
* 05 增加 删除 修改 查询 清空
*/
class Node {
constructor(element, next) {
this.element = element
this.next = next
}
}
class LinkedList {
constructor(head, size) {
this.head = null
this.size = 0
}
// 获取节点的位置
_getNode(index) {
if (index < 0 || index >= this.size) {
throw new Error('越界了')
}
let currentNode = this.head
for (let i = 0; i < index; i++) {
currentNode = currentNode.next
}
return currentNode
}
// 增加
add(index, element) {
if (arguments.length === 1) {
element = index
index = this.size
}
if (index < 0 || index > this.size) {
throw new Error('cross the border')
}
if (index === 0) {
let head = this.head // 保存原有 head 的指向
this.head = new Node(element, head)
} else {
let prevNode = this._getNode(index - 1)
prevNode.next = new Node(element, prevNode.next)
}
this.size++
}
// 删除
remove(index) {
let rmNode = null
if (index === 0) {
rmNode = this.head
if (!rmNode) {
return undefined
}
this.head = rmNode.next
} else {
let prevNode = this._getNode(index -1)
rmNode = prevNode.next
prevNode.next = rmNode.next
}
this.size--
return rmNode
}
// 修改
set(index, element) {
let node = this._getNode(index)
node.element = element
}
// 查询
get(index) {
return this._getNode(index)
}
// 清空
clear() {
this.head = null
this.size = 0
}
}
class Queue {
constructor() {
this.LinkedList = new LinkedList()
}
enQueue(data) {
this.LinkedList.add(data)
}
deQueue() {
return this.LinkedList.remove(0)
}
}
const q = new Queue()
q.enQueue('node1')
q.enQueue('node2')
let a = q.deQueue()
a = q.deQueue()
a = q.deQueue()
console.log(a);
// undefined
37、文件可写流实现
const fs = require('fs')
const EventsEmitter = require('events')
const Queue = require('./linkedlist')
class MyWriterStream extends EventsEmitter {
constructor(path, options={}) {
super()
this.path = path
this.flags = options.flags || 'w'
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.encoding = options.encoding || 'uft-8'
this.highWaterMark = options.highWaterMark || 16*1024
this.open()
this.writeoffset = this.start
this.writing = false
this.writeLen = 0
this.needDrain = false
this.cache = new Queue()
}
open() {
// 原生 fs.open
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
}
// 正常打开文件
this.fd = fd
this.emit('open', fd)
})
}
write(chunk, encoding, cb) {
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
this.writeLen += chunk.length
let flag = this.writeLen < this.highWaterMark
this.needDrain = !flag
if (this.writing) {
// 当前正在执行写入,所以内容应该排队
this.cache.enQueue({ chunk, encoding, cb})
} else {
this.writing = true
// 当前不是正在写入,那么就执行写入
this._write(chunk, encoding, () => {
cb()
// 清空排队的内容
this._clearBuffer()
})
}
return flag
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once('open', () => {
return this._write(chunk, encoding, cb)
})
}
fs.write(this.fd, chunk, this.start, chunk.length, this.writeoffset, (err, written) => {
this.writeoffset += written
this.writeLen -= written
cb && cb()
})
}
_clearBuffer() {
let data = this.cache.deQueue()
if (data) {
this.write(data.element.chunk, data.element.encoding, () => {
data.element.cb && data.element.cb()
this._clearBuffer()
})
} else {
if (this.needDrain) {
this.needDrain = false
this.emit('drain')
}
}
}
}
const ws = new MyWriterStream('./f9.txt', {
highWaterMark: 1
})
ws.on('open', (fd) => {
console.log('open--->', fd);
})
let flag = ws.write('1', 'utf8', () => {
console.log('ok1');
})
// console.log(flag)
flag = ws.write('10', 'utf8', () => {
console.log('ok1');
})
// console.log(flag)
ws.on('drain', () => {
console.log('drain');
})
38、pipe方法使用
// ReadStream.js
const fs = require('fs')
const EventEmitter = require('events')
class MyReadStream extends EventEmitter{
constructor(path, options = {}) {
super()
this.path = path
this.flags = options.flags || "r"
this.mode = options.mode || 438
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end
this.highWaterMark = options.highWaterMark || 64 * 1024
this.readOffset = 0
this.open()
this.on('newListener', (type) => {
if (type === 'data') {
this.read()
}
})
}
open() {
// 原生 open 方法来打开指定位置上的文件
fs.open(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
this.emit('error', err)
}
this.fd = fd
this.emit('open', fd)
})
}
read() {
if (typeof this.fd !== 'number') {
return this.once('open', this.read)
}
let buf = Buffer.alloc(this.highWaterMark)
let howMuchToRead
/* if (this.end) {
howMuchToRead = Math.min(this.end - this.readOffset + 1, this.highWaterMark)
} else {
howMuchToRead = this.highWaterMark
} */
howMuchToRead = this.end ? Math.min(this.end - this.readOffset + 1, this.highWaterMark) : this.highWaterMark
fs.read(this.fd, buf, 0, howMuchToRead, this.readOffset, (err, readBytes) => {
if (readBytes) {
this.readOffset += readBytes
this.emit('data', buf.slice(0, readBytes))
this.read()
} else {
this.emit('end')
this.close()
}
})
}
close() {
fs.close(this.fd, () => {
this.emit('close')
})
}
pipe(ws) {
this.on('data', (data) => {
let flag = ws.write(data)
console.log(flag);
if (!flag) {
this.pause()
}
})
ws.on('drain', () => {
this.resume()
})
}
}
module.exports = MyReadStream
const fs = require('fs')
const myReadStream = require('./ReadStream')
// const rs = fs.createReadStream('./f9.txt', {
// highWaterMark: 4
// })
const rs = new myReadStream('./f9.txt')
const ws = fs.createWriteStream('./f10.txt')
rs.pipe(ws)
// data