核心模块之 Events
EventEmitter 常见 API
- on:添加当事件被触发时调用的回调函数
- emit:触发事件,按照注册的顺序同步调用每个事件监听器
- once:添加当事件在注册之后首次被触发时调用的回调函数
- off:移除特定的监听器
发布订阅模式
class PubSub{
constructor() {
this._events = {}
}
// 注册
subscribe(event, callback) {
if (this._events[event]) {
// 如果当前 event 存在,所以我们只需要往后添加当前次监听操作
this._events[event].push(callback)
} else {
// 之前没有订阅过此事件
this._events[event] = [callback]
}
}
// 发布
publish(event, ...args) {
const items = this._events[event]
if (items && items.length) {
items.forEach(function (callback) {
callback.call(this, ...args)
})
}
}
}
let ps = new PubSub()
ps.subscribe('事件1', () => {
console.log('事件1执行了')
})
ps.subscribe('事件1', () => {
console.log('事件1执行了---2')
})
ps.publish('事件1')
ps.publish('事件1')
EventEmitter 模拟实现
function MyEvent () {
// 准备一个数据结构用于缓存订阅者信息
this._events = Object.create(null)
}
MyEvent.prototype.on = function (type, callback) {
// 判断当前次的事件是否已经存在,然后再决定如何做缓存
if (this._events[type]) {
this._events[type].push(callback)
} else {
this._events[type] = [callback]
}
}
MyEvent.prototype.emit = function (type, ...args) {
if (this._events && this._events[type].length) {
this._events[type].forEach((callback) => {
callback.call(this, ...args)
})
}
}
MyEvent.prototype.off = function (type, callback) {
// 判断当前 type 事件监听是否存在,如果存在则取消指定的监听
if (this._events && this._events[type]) {
this._events[type] = this._events[type].filter((item) => {
return item !== callback && item.link !== callback
})
}
}
MyEvent.prototype.once = function (type, callback) {
let foo = function (...args) {
callback.call(this, ...args)
this.off(type, foo)
}
foo.link = callback
this.on(type, foo)
}
let ev = new MyEvent()
let fn = function (...data) {
console.log('事件1执行了', data)
}
/* ev.on('事件1', fn)
ev.on('事件1', () => {
console.log('事件1----2')
})
ev.emit('事件1', 1, 2)
ev.emit('事件1', 1, 2) */
/* ev.on('事件1', fn)
ev.emit('事件1', '前')
ev.off('事件1', fn)
ev.emit('事件1', '后') */
ev.once('事件1', fn)
// ev.off('事件1', fn)
ev.emit('事件1', '前')
浏览器中的 Eventloop
完整事件循环执行顺序
- 从上至下执行所有的同步代码
- 执行中遇到的宏任务与微任务加至相应的队列
- 同步代码执行完毕后,执行满足条件的微任务回调
- 微任务队列执行完毕后执行所有满足需求的宏任务回调
- 循环事件环操作
- 注意:每执行一个宏任务之后就会立刻检查微任务队列
Nodejs 下的事件循环
Nodejs 事件循环机制
队列说明
- timers:执行 setTimout 与 setInterval 回调
- pending callback:执行系统操作的回调,例如 tcp udp
- idle,prepare:只在系统内部进行使用
- poll:执行与 I/O相关的回调
- check:执行 setImmediate 中的回调
- close callbacks:执行 close 事件的回调
Nodejs 完整事件循环
- 执行同步代码,将不同的任务添加至相应的队列
- 所有同步代码执行后会去执行满足条件微任务
- 所有微任务代码执行后会执行 timer 队列中满足的宏任务
- timer 中的所有宏任务执行完成后就会依次切换队列
- 注意:在完成队列切换之前会先清空微任务代码
Nodejs 与浏览器事件循环区别
-
任务队列数不同
- 浏览器中只有2个任务队列
- Nodejs 中有6个事件队列
-
Nodejs 微任务执行时机不同
- 二者都会在同步代码执行完成后执行微任务
- 浏览器平台下每当一个宏任务执行完毕后就会清空微任务
- Nodejs 平台在事件队列切换时会去清空微任务
-
微任务优先级不同
- 浏览器环境中,微任务存放于事件队列,先进先出
- Nodejs 中 process.nextTick 先于 promise.then
核心模块之 Stream
流处理数据的优势
- 时间效率:流的分段处理可以同时操作多个数据 chunk
- 空间效率:同一时间流无须占据大内存空间
- 使用方便:流配合管理,扩展程序变的简单
Node.js 中流的分类
-
Readable:可读流,能够实现数据的读取
- 自定义可读流
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()) } }) */ myReadable.on('data', (chunk) => { console.log(chunk.toString()) })
-
Writable:可写流,能够实现数据的写操作
- 自定义可写流
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') })
- 可写流事件
- pipe 事件:可读流调用 pipe() 方法时触发
- unpipe 事件:可读流调用 unpipe() 方法时触发
- drain 事件:write 返回 false ,数据执行可写入时触发
-
Duplex:双工流,既可读又可写
- 自定义双工流
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()) }) */ myDuplex.write('拉勾教育', () => { console.log(1111) })
-
Transform:转换流,可读可写,还能实现数据转换
- 自定义转换流
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()) })
Node.js 流特点
- Stream 模块实现了具体的四个抽象
- 所有流都继承自 EventEmitter
背压机制
let fs = require('fs')
let rs = fs.createReadStream('test.txt', {
highWaterMark: 4
})
let ws = fs.createWriteStream('test1.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()
}) */
rs.pipe(ws)
模拟文件可读流
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')
})
}
pipe(ws){
this.on('data', (data) => {
let flag = ws.write(data)
if(!flag){
this.pause()
}
})
ws.on('drain', () => {
this.resume()
})
}
}
let rs = new MyFileReadStream('test.txt', {
end: 7,
highWaterMark: 3
})
rs.on('data', (chunk) => {
console.log(chunk)
})
链表结构
为什么不采取数组存储数据
- 数组存储数据的长度具有上限
- 数组存在塌陷问题
单向链表实现
/*
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)
文件可写流实现
const fs = require('fs')
const EventsEmitter = require('events')
const Queue = require('./linkedlist')
class MyWriteStream 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 || 'utf8'
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 MyWriteStream('./f9.txt', {})
ws.on('open', (fd) => {
console.log('open---->', fd)
})
let flag = ws.write('1', 'utf8', () => {
console.log('ok1')
})
flag = ws.write('10', 'utf8', () => {
console.log('ok1')
})
flag = ws.write('拉勾教育', 'utf8', () => {
console.log('ok3')
})
ws.on('drain', () => {
console.log('drain')
})
pipe 方法的使用实现
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)