一、核心模块path
- basename() 获取路径中基础名称
/**
* 01 返回的就是接收路径当中的最后一部分
* 02 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀
* 03 第二个参数做为后缀时,如果没有在当前路径中被匹配到,那么就会忽略
* 04 处理目录路径的时候如果说,结尾处有路径分割符,则也会被忽略掉
*/
console.log(path.basename(__filename))
console.log(path.basename(__filename, '.js'))
console.log(path.basename(__filename, '.css'))
console.log(path.basename('/a/b/c'))
console.log(path.basename('/a/b/c/'))
- dirname() 获取路径中目录名称
/**
* 01 返回路径中最后一个部分的上一层目录所在路径
*/
console.log(path.dirname(__filename))
console.log(path.dirname('/a/b/c'))
console.log(path.dirname('/a/b/c/'))
- extname()获取路径中扩展名称
/**
* 01 返回 path路径中相应文件的后缀名
* 02 如果 path 路径当中存在多个点,它匹配的是最后一个点,到结尾的内容
*/
console.log(path.extname(__filename))
console.log(path.extname('/a/b'))
console.log(path.extname('/a/b/index.html.js.css'))
console.log(path.extname('/a/b/index.html.js.'))
- isAbsolute() 获取路径是否为绝对路径
console.log(path.isAbsolute('foo')) // false
console.log(path.isAbsolute('/foo')) // true
console.log(path.isAbsolute('///foo'))
console.log(path.isAbsolute(''))
console.log(path.isAbsolute('.'))
console.log(path.isAbsolute('../bar'))
- join() 拼接多个路径片段
console.log(path.join('a/b', 'c', 'index.html'))
console.log(path.join('/a/b', 'c', 'index.html'))
console.log(path.join('/a/b', 'c', '../', 'index.html'))
console.log(path.join('/a/b', 'c', './', 'index.html'))
console.log(path.join('/a/b', 'c', '', 'index.html'))
console.log(path.join(''))
- resolve() 返回绝对路径
/**
* resolve([from], to)
*/
console.log(path.resolve('/a', '../b'))
console.log(path.resolve('index.html'))
- pasre() 解析路径
/**
* 01 接收一个路径,返回一个对象,包含不同的信息
* 02 root dir base ext name
*/
const obj = path.parse('/a/b/c/index.html')
// const obj = path.parse('/a/b/c/')
// const obj = path.parse('./a/b/c/')
console.log(obj)
- format() 序列化路径
const obj = path.parse('./a/b/c/')
console.log(path.format(obj)) // ./a/b\c
- normalize() 规范化路径
console.log(path.normalize(''))
console.log(path.normalize('a/b/c/d'))
console.log(path.normalize('a///b/c../d'))
console.log(path.normalize('a//\\/b/c\\/d'))
console.log(path.normalize('a//\b/c\\/d'))
二、全局变量Buffer
1. Buffer全局变量
Buffer可以让Javascript操作二进制(二进制数据、流操作、Buffer)。
起初Javascript语言服务于浏览器平台,Nodejs平台下JavaScript可以实现IO操作(FS 等)。
IO行为操作的就是二进制数据,Stream流操作并非Nodejs独创,我们可以把它理解为一种数据类型(如数组、字符串)。
流操作配合管道实现数据分段传输,数据的端到端传输会生产者和消费者,生产和消费的过程往往存在等待。
产生等待时数据存在在哪?(Buffer)Nodejs中Buffer是一片内存空间(C++),它不占据V8堆内存的大小,但由JS分配,由v8引擎的GC回收。
2. 创建Buffer(类)
- alloc:创建指定字节大小的 buffer
const b1 = Buffer.alloc(10)
console.log(b1) // <Buffer 00 00 00 00 00 00 00 00 00 00>
- allocUnsafe:创建指定大小的 buffer(不安全),只要有内存空间就会使用
const b2 = Buffer.allocUnsafe(10)
console.log(b2) // <Buffer d0 d8 49 b8 3e 02 00 00 28 d9>
- from:接收数据,创建 buffer(编码可选--'utf-8'),接收字符串或数组参数作为数据。
const b1 = Buffer.from('中', 'utf-8')
console.log(b1) // <Buffer e4 b8 ad>
const b2 = Buffer.from([0xe4, 0xb8, 0xad])
console.log(b2) // <Buffer e4 b8 ad>
console.log(b2.toString('utf-8')) // 中
const b3 = Buffer.alloc(3)
const b4 = Buffer.from(b3)
console.log(b3) // <Buffer 00 00 00>
console.log(b4) // <Buffer 00 00 00>
// 两者的内存空间不共享的
b3[0] = 1
console.log(b3) // <Buffer 01 00 00>
console.log(b4) // <Buffer 00 00 00>
3. Buffer实例方法
- fill:使用数据填充 buffer
// buf.fill(123) // <Buffer 7b 7b 7b 7b 7b 7b>
buf.fill('12345678', 1, 3) // 填充数据 开始 结束 (完全填充)
console.log(buf) // <Buffer 00 31 32 00 00 00>
console.log(buf.toString()) // 12
- write:向 buffer 中写入数据
buf.write('123', 1, 4) // 填充数据 开始 长度(一般填充)
console.log(buf) // <Buffer 00 31 32 33 00 00>
console.log(buf.toString()) // 123
- toString: 从 buffer 中提取数据
buf = Buffer.from('拉勾教育')
console.log(buf) // <Buffer e6 8b 89 e5 8b be e6 95 99 e8 82 b2>
console.log(buf.toString('utf-8', 3, 9)) // 字符编码 开始 结束 (勾教)
- slice: 截取 buffer
buf = Buffer.from('拉勾教育')
let b1 = buf.slice(-3)
console.log(b1) // <Buffer e8 82 b2>
console.log(b1.toString()) // 育
- indexOf: 在 buffer 中查找数据
buf = Buffer.from('zcesaddwewqrqggnddd')
console.log(buf)
console.log(buf.indexOf('qc', 4)) // -1
- copy:拷贝 buffer 中的数据
let b1 = Buffer.alloc(6)
let b2 = Buffer.from('拉勾')
b2.copy(b1, 3, 3, 6) // 将b2拷贝到b1中 开始写入位置 开始读取位置 长度
console.log(b1.toString())
console.log(b2.toString())
4. Buffer静态方法
- concat:将多个 buffer 拼接成一个新的 buffer
let b1 = Buffer.from('hello')
let b2 = Buffer.from('world')
let b = Buffer.concat([b1, b2], 9)
console.log(b)
console.log(b.toString()) // helloworl
- isBuffer:判断当前数据是否为 buffer
let b1 = '123'
console.log(Buffer.isBuffer(b1)) // false
5. Buffer-split实现
ArrayBuffer.prototype.split = function (sep) {
let len = Buffer.from(sep).length
let ret = []
let start = 0
let offset = 0
while (offset = this.indexOf(sep, start) !== -1) {
ret.push(this.slice(start, offset))
start = offset + len
}
ret.push(this.slice(start))
return ret
}
let buf = '哈皮,凉皮,好顽皮'
let bufArr = buf.split('皮')
console.log(bufArr) // [ '哈', ',凉', ',好顽', '' ]
三、核心模块FS
1. 前置知识
用户对于文件所具备的操作权限:R(读-8)W(写-4)S(执行-1)(不具备权限-0)。 操作系统将用户分为3类:文件所有者(自己)、文件所属组(家人)、访客(陌生人)。
2. Nodejs中 flag表示对文件操作方式,常见 flag 操作符
- r:表示可读
- w:表示可写
- s:表示同步
- +:表示执行相反操作
- x:表示排它操作
- a:表示追加操作
3. fs介绍
- fd就是操作系统分配给被打开文件的标识(描述符从3开始的)
- 代码层面上fs分为基本操作类和常用API
- 文件操作概念:权限位、标识符、操作符
4. 文件操作API
- readFile:从指定文件中读取数据
fs.readFile(path.resolve('data1.txt'), 'utf-8', (err, data) => {
// 在Nodejs中错误优先
console.log(err)
if (!null) {
console.log(data)
}
})
- writeFile:向指定文件中写入数据
fs.writeFile('data.txt', '123', {
mode: 438,
flag: 'w+',
encoding: 'utf-8'
}, (err) => {
if (!err) {
fs.readFile('data.txt', 'utf-8', (err, data) => {
console.log(data)
})
}
})
- appendFile: 追加的方式向指定文件中写入数据
fs.appendFile('data.txt', 'hello node.js',{}, (err) => {
console.log('写入成功')
})
- copyFile:将某个文件中的数据拷贝至另一文件
fs.copyFile('data.txt', 'test.txt', () => {
console.log('拷贝成功')
})
- watchFile:对指定文件进行监控
fs.watchFile('data.txt', { interval: 20 }, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
console.log('文件被修改了')
fs.unwatchFile('data.txt')
}
})
5. md转html实现-核心代码
// 核心代码
fs.watchFile(mdPath, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
fs.readFile(mdPath, 'utf-8', (err, data) => {
// 将 md--》html
let htmlStr = marked(data)
fs.readFile(cssPath, 'utf-8', (err, data) => {
let retHtml = temp.replace('{{content}}', htmlStr).replace('{{style}}', data)
// 将上述的内容写入到指定的 html 文件中,用于在浏览器里进行展示
fs.writeFile(htmlPath, retHtml, (err) => {
console.log('html 生成成功了')
})
})
})
}
})
6. 文件打开与关闭 (作用:适应大文件的操作, readFile等是一次性内存操作)
// open
fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
console.log(fd) // 3 (打开了)
})
// close
fs.open('data.txt', 'r', (err, fd) => {
console.log(fd)
fs.close(fd, err => {
console.log('关闭成功')
})
})
7. 大文件读写操作(一次性读写无须Buffer, 大文件边读边写需Buffer)
const fs = require('fs')
// read : 所谓的读操作就是将数据从磁盘文件中写入到 buffer 中
let buf = Buffer.alloc(10)
/**
* fd 定位当前被打开的文件
* buf 用于表示当前缓冲区
* offset 表示当前从 buf 的哪个位置开始执行写入
* length 表示当前次写入的长度
* position 表示当前从文件的哪个位置开始读取
*/
fs.open('data.txt', 'r', (err, rfd) => {
console.log(rfd)
fs.read(rfd, buf, 1, 4, 3, (err, readBytes, data) => {
console.log(readBytes)
console.log(data)
console.log(data.toString())
})
})
// write 将缓冲区里的内容写入到磁盘文件中
buf = Buffer.from('1234567890')
fs.open('b.txt', 'w', (err, wfd) => {
fs.write(wfd, buf, 2, 4, 0, (err, written, buffer) => {
console.log(written, '----')
fs.close(wfd)
})
})
8. 文件拷贝自定义实现(处理大文件)
/**
* 01 打开 a 文件,利用 read 将数据保存到 buffer 暂存起来
* 02 打开 b 文件,利用 write 将 buffer 中数据写入到 b 文件中
*/
const BUFFER_SIZE = buf.length
let readOffset = 0
fs.open('a.txt', 'r', (err, rfd) => {
fs.open('b.txt', 'w', (err, wfd) => {
function next() {
fs.read(rfd, buf, 0, BUFFER_SIZE, readOffset, (err, readBytes) => {
if (!readBytes) {
// 如果条件成立,说明内容已经读取完毕
fs.close(rfd, () => { })
fs.close(wfd, () => { })
console.log('拷贝完成')
return
}
readOffset += readBytes
fs.write(wfd, buf, 0, readBytes, (err, written) => {
next()
})
})
}
next()
})
})
四、FS目录操作
1. 目录操作API
- access:判断文件或目录是否具有操作权限
fs.access('a.txt', (err) => {
if (err) {
console.log(err)
} else {
console.log('有操作权限')
}
})
- stat:获取目录及文件信息
fs.stat('a.txt', (err, statObj) => {
console.log(statObj.size)
console.log(statObj.isFile())
console.log(statObj.isDirectory())
})
- mkdir:创建目录
fs.mkdir('a/b/c', {recursive: true}, (err) => {
if (!err) {
console.log('创建成功')
}else{
console.log(err)
}
})
- rmdir:删除目录
fs.rmdir('a', {recursive: true}, (err) => {
if (!err) {
console.log('删除成功')
} else {
console.log(err)
}
})
- readdir:读取目录中内容
fs.readdir('a/b', (err, files) => {
console.log(files)
})
- unlink:删除指定文件
fs.unlink('a/a.txt', (err) => {
if (!err) {
console.log('删除成功')
}
})
五、Nodejs模块化
1. 模块化历程
传统开发常见问题:命名冲突和污染、代码冗余,无效请求多、文件间的依赖关系复杂,导致项目难以维护,不可复用。模块是小而精且可利用维护的代码块,可以利用函数、对象、执行函数拆分。
Commonjs规范类似ECMAscript规范。Commonjs规范是同步加载模块,不适用浏览器加载。
AMD规范(require.js)、CMD规范(sea.js)、ES modules规范(ES6)。
2. Commonjs规范
Commonjs是语言层面的规范:模块引用、模块定义、模块标识。
Nodejs语言的实现:任一文件都是模块,具有独立作用域;使用require导入其他模块;将模块ID传入require 实现目标模块的定位。
- CommonJS 规范起初是为了弥补JS语言模块化缺陷
- CommonJS 是语言层面的规范,当前主要用于Node.js
- CommonJS 规定模块化分为引入、定义、标识符三个部分
- Moudle 在任意模块中可直接使用包含模块信息
- Require接收标识符,加载目标模块
- Exports 与 module.exports 都能导出模块数据
- CommonJS规范定义模块的加载是同步完成
3. module 属性
- 任意js文件就是一个模块,可以直接使用 module 属性
- id:返回模块标识符,一般是一个绝对路径
- filename:返回文件模块的绝对路径
- loaded:返回布尔值,表示模块是否完成加载
- parent: 返回对象存放调用当前模块的模块
- children: 返回数组,存放当前模块调用的其它模块
- exports: 返回当前模块需要暴露的内容
- paths:返回数组,存放不同目录下的 node_modules 位置
4. module.export 与 exports的区别?
exports是Nodejs为了便于使用,定义的一个指向module.export内存地址的变量,不能被赋值为其他数值,否则就切断了和module.export的联系
5. require属性
- 基本功能是读入并且执行一个模块文件
- resolve: 返回模块文件绝对路径
- extensions: 依据不同后缀名执行解析操作
- main:返回主模块对象
6. Nodejs模块分类
- 内置模块:加载速度(Node源码编译时写入到二进制文件)、
- 文件模块:加载速度(代码运行时,动态加载)
7. 模块加载流程
- 路径分析:依据标识符确定模块位置。路径标识符,非路径标识符(常见于核心模块 如fs),模块路径(路径依次向上查找node_modules,直到盘符根)
- 文件定位:确定目标模块中具体的文件及文件类型。
- 编译执行:采用对应的方式完成文件的编译执行。
- 使用 fs 模块同步读入目标文件内容
- 对内容进行语法包装,生成可执行JS函数
- 调用函数时传入 exports、module、require 等属性值
- 以上3点是对JS文件的编译,对JSON文件编译是将读取到的内容通过JSON.parse()进行解析。
- 缓存优先原则
- 提高模块加载速度
- 当前模块不存在,则经历一次完整加载流程
- 模块加载完成后,使用路径作为索引进行缓存
六、模块加载分析
1. 源码分析及内置模块VM(创建独立运行的沙箱环境)
const fs = require('fs')
const vm = require('vm')
let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')
// eval
// eval(content)
// new Function
/* console.log(age)
let fn = new Function('age', "return age + 1")
console.log(fn(age)) */
// vm.runInThisContext(content)
// 默认无法使用外部局部变量,但可以使用全局变量
vm.runInThisContext("age += 10")
console.log(age)
2. 模块加载流程实现(以文件加载为例)
- 路径分析
- 缓存优化
- 文件定位
- 编译执行
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function Module (id) {
this.id = id
this.exports = {}
console.log(1111)
}
Module._resolveFilename = function (filename) {
// 利用 Path 将 filename 转为绝对路径
let absPath = path.resolve(__dirname, filename)
// 判断当前路径对应的内容是否存在()
if (fs.existsSync(absPath)) {
// 如果条件成立则说明 absPath 对应的内容是存在的
return absPath
} else {
// 文件定位
let suffix = Object.keys(Module._extensions)
for(var i=0; i<suffix.length; i++) {
let newPath = absPath + suffix[i]
if (fs.existsSync(newPath)) {
return newPath
}
}
}
throw new Error(`${filename} is not exists`)
}
Module._extensions = {
'.js'(module) {
// 读取
let content = fs.readFileSync(module.id, 'utf-8')
// 包装
content = Module.wrapper[0] + content + Module.wrapper[1]
// VM
let compileFn = vm.runInThisContext(content)
// 准备参数的值
let exports = module.exports
let dirname = path.dirname(module.id)
let filename = module.id
// 调用
compileFn.call(exports, exports, myRequire, module, filename, dirname)
},
'.json'(module) {
let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
module.exports = content
}
}
Module.wrapper = [
"(function (exports, require, module, __filename, __dirname) {",
"})"
]
Module._cache = {}
Module.prototype.load = function () {
let extname = path.extname(this.id)
Module._extensions[extname](this)
}
function myRequire (filename) {
// 1 绝对路径
let mPath = Module._resolveFilename(filename)
// 2 缓存优先
let cacheModule = Module._cache[mPath]
if (cacheModule) return cacheModule.exports
// 3 创建空对象加载目标模块
let module = new Module(mPath)
// 4 缓存已加载过的模块
Module._cache[mPath] = module
// 5 执行加载(编译执行)
module.load()
// 6 返回数据
return module.exports
}
let obj = myRequire('./v')
let obj2 = myRequire('./v')
console.log(obj.age)
七、事件模块Events
通过 EventEmitter 类实现事件统一管理。
1. events 与 EventEmitter
- Nodejs 是基于事件驱动的异步操作架构,内置events模块
- events 模块提供了EventEmitter 类
- Nodejs 中很多内置核心模块继承EventEmitter(如 FS、HTTP、NET)
2. EventEmitter 类常见API
- on:添加当事件被触发时调用的回调函数
- emit:触发事件,按照注册的序同步调用每个事件监听器
- once:添加当事件在注册之后首次被触发时调用的回调函数
- off:移除特定的监听器
const EventEmitter = require('events')
const ev = new EventEmitter()
ev.on('事件1', function () {
console.log(this)
})
ev.on('事件1', function () {
console.log(2222)
})
ev.on('事件2', function () {
console.log(333)
})
ev.emit('事件1')
打印this的值:
3. 发布订阅(定义对象间一对多的依赖关系)
主要解决问题:在没有Promise的时候,事件触发后有一连串的异步操作,这些异步又相互依赖操作结果。
工作流程:发布者发布事件,统一由调度中心,调用之前的订阅代码执行。
4. 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.once('事件1', fn)
// ev.off('事件1', fn)
ev.emit('事件1', '前')
八、事件环
1. 浏览器中的事件环
setTimeout(() => {
console.log('s1')
Promise.resolve().then(() => {
console.log('p2')
})
Promise.resolve().then(() => {
console.log('p3')
})
})
Promise.resolve().then(() => {
console.log('p1')
setTimeout(() => {
console.log('s2')
})
setTimeout(() => {
console.log('s3')
})
})
// p1 s1 p2 p3 s2 s3
完整事件环执行顺序:
- 从上至下执行所有的同步代码.
- 执行过程中将遇到的宏任务与微任务添加至相应的队列.
- 同步代码执行完毕后,执行满足条件的微任务回调.
- 微任务队列执行完毕后执行所有满足需求的宏任务回调.
- 事件环操作
注意:宏任务之后就会立刻检查微任务队列
2. Nodejs中的事件环
Nodejs的6个事件队列:每一个队列中存放的都是回调函数,与浏览器一个宏任务不同。他们都有微任务队列。
- timers: 执行setTimout 与setInterval回调
- pending callbacks: 执行操作系统的回调,例如 tcp udp (暂不用考虑)
- idle,prepare: 只在系统内部使用 (暂不用考虑)
- poll: 执行与I/O相关的回调
- check: 执行setImmediate中的回调
- close callbacks: 执行close事件回调 (暂不用考虑)
Nodejs完整事件环的执行:
- 执行同步代码,将不同的任务添加至相应的队列
- 所有同步代码执行后会去执行满足条件微任务
- 所有微任务代码执行后会执行timer队列中满足的宏任务
- timer中的所有宏任务执行完成后就会依次切换队列
注意:在完成队列切换之前会先清空微任务代码。优先级 process.nextTick() > Promise 是Nodejs平台的微任务。
Nodejs事件环梳理:(新版Node改为每执行完一个宏任务就清空微任务队列,保持和浏览器一致)
3. Nodejs器事件环常见问题
九、核心模块Stream
文件操作系统和网络模块实现了流接口。Nodejs中的流就是处理流式数据的抽象接口。
1. 为什么使用流来处理数据?
问题:
- 同步读取资源文件,用户需要等待数据读取完成。
- 资源文件最终一次性加载至内存,开销比较大
优势:
- 时间效率:流的分段处理可以同时操作多个数据chunk
- 空间效率:同一时间无须占用大内存空间
- 使用方便:流配合管道连接,扩展程序比较简单
流的分类(以下4个是具体的抽象,所有的流都继承了EventEmitter):
- Readable: 可读流,能够实现数据的读取
- Writeable: 可写流,能够事件数据的写操作
- Duplex: 双工流,即可写也可读(如 Net 模块内的socket)
- Tranform: 转换流,可读可写,还能实现数据转换(如 Zlib 下的createDeffort)
示例:
const fs = require('fs')
let rs = fs.createReadStream('./test.txt')
let ws = fs.createWriteStream('./test1.txt')
// 执行拷贝行为
rs.pipe(ws)
2. 可读流
可读流是生产供程序消费数据的流。
自定义可读流:
- 继承stream里Readable
- 重写_read方法调用push方法将读取的数据添加到缓冲区(一个链表结构) 自定义可读流的问题:
- 底层数据读取完成后该如何处理?给数组的最后添加一个空值
- 消费者如何获取可读流中的数据?Readable提供了两个事件:readable事件、data事件,为了满足不同的数据使用场景,因此有流动模式、暂停模式。
消费数据:
- reabable事件:当缓存区准备好,也就是当流中存在可读取数据时触发。直到消费者拿到null之后,标识底层数据已经读取完。
- data事件:可读流处于流动模式,当流中数据块传给消费者后触发。同样读取到null之后,消费行为结束。
- end事件:数据被消费完之后触发。
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) {
// 依次打印 lg zc es yy
console.log(data.toString())
}
})
// 流动模式 (可能都不需要往缓存中添加)
// myReadable.on('data', (chunk) => {
// //依次打印 lg zce syy
// console.log(chunk.toString())
// })
myReadable.on('end', (chunk) => {
console.log('end: 消费结束');
})
3. 可写流
可写流是用于消费数据的流。 自定义可写流:
- 继承stream里Writeable
- 重写_write方法调用write方法执行写入 可写流事件:
- pipe事件:可读流调用 pipe() 方法时触发
- unpipe事件:可读流调用 unpipe()方法时触发
- drain事件:中文译为抽干
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('AABBCC', 'utf-8', () => {
console.log('end')
})
4. 双工(Duplex)和转换流(Transform)
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('AABBXCC', () => {
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())
})
十、文件可读流和可写流
1. 文件可读流的创建、消费、事件、应用
const fs = require('fs')
let rs = fs.createReadStream('test.txt', {
flags: 'r',
encoding: null,
fd: null,
mode: 438,
autoClose: true,
start: 0,
// end: 3,
highWaterMark: 4
})
/* rs.on('data', (chunk) => {
console.log(chunk.toString())
rs.pause()
setTimeout(() => {
rs.resume()
}, 1000)
}) */
/* rs.on('readable', () => {
// let data = rs.read()
// console.log(data)
let data
while((data = rs.read(1)) !== null) {
console.log(data.toString())
console.log('----------', rs._readableState.length)
}
}) */
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('出错了')
})
2. 文件可写流
- 创建可写流
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')
// 字符串 或者 buffer ===》 fs rs
/* ws.write(buf, () => {
console.log('ok2')
}) */
/* ws.write('AABB', () => {
console.log('ok1')
}) */
/* ws.on('open', (fd) => {
console.log('open', fd)
}) */
ws.write("2")
// close 是在数据写入操作全部完成之后再执行
/* ws.on('close', () => {
console.log('文件关闭了')
}) */
// end 执行之后就意味着数据写入操作完成
ws.end('AABB')
// error
ws.on('error', (err) => {
console.log('出错了')
})
- write执行流程
- 控制写入速度
/**
* 需求:“AABBCC” 写入指定的文件
* 01 一次性写入
* 02 分批写入
* 对比:
*/
let fs = require('fs')
let ws = fs.createWriteStream('test.txt', {
highWaterMark: 3
})
// ws.write('AABBCC')
let source = "AABBCC".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()
})
- 背压机制 Nodejs的stream已经实现了背压机制。
存在问题:生产者产生数据的速度远远大于消费者消费数据的速度,readable内部维护了一个队列,将展示无法消费的数据缓存到队列中。但是队列的内存大小是有上限的(默认16kb,文件可读流64kb),因此读写的过程中不实现一个背压的机制,很有可能会出现内存溢出、GC频繁调用,其他进程变慢。
数据的读操作图示:
数据写操作图示:
解析pipe方法背压机制内部的原理:
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)
3. 模拟文件可读流
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 = 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)
})
4. 链表结构(一系列节点的集合)
数组相对链表储存队列的缺陷:
- 在多个语言下,数组存放数据的长度具有上限。
- 数组在插入和删除元素时,都会移动其他元素的位置。
- 在JS中数组被实现成了一个对象,在使用效率上会低一些。
链表分类:
- 双向链表
- 单向链表
- 循环链表
单向链表(默认head指向null):
双向链表就是在element节点上增加一个prev属性。循环链表是将首位链接起来。
单向链表实现:
/*
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')
// l1.remove(1)
l1.set(1, 'node3-3')
// let a = l1.get(0)
l1.clear()
console.log(l1)
单向链表实现队列(先进先出-可写流的readable):
// 接上代码
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)