内置模块
Path
内置模块,require之后直接使用
主要是处理文件、目录的路径
常用API
代码演示
const path = require('path')
// console.log(__filename)
// 1 获取路径中的基础名称
/**
* 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/')) */
// 2 获取路径目录名 (路径)
/**
* 01 返回路径中最后一个部分的上一层目录所在路径
*/
/* console.log(path.dirname(__filename))
console.log(path.dirname('/a/b/c'))
console.log(path.dirname('/a/b/c/')) */
// 3 获取路径的扩展名
/**
* 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.')) */
// 4 解析路径
/**
* 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.name) */
// 5 序列化路径
/* const obj = path.parse('./a/b/c/')
console.log(path.format(obj)) */
// 6 判断当前路径是否为绝对
/* console.log(path.isAbsolute('foo'))
console.log(path.isAbsolute('/foo'))
console.log(path.isAbsolute('///foo'))
console.log(path.isAbsolute(''))
console.log(path.isAbsolute('.'))
console.log(path.isAbsolute('../bar')) */
// 7 拼接路径
/* 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('')) */
// 8 规范化路径
/* 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')) */
// 9 绝对路径
// console.log(path.resolve())
/**
* resolve([from], to)
*/
// console.log(path.resolve('/a', '../b'))
console.log(path.resolve('index.html'))
全局变量buffer
Buffer缓冲区Buffer让JS可以操作二进制
为什么使用buffer?
因为Nodejs平台下JavaScript可实现IO操作,IO相关信息(文本,视频,音频,字符,图片等)都是二进制的表现形式
Stream流操作并非Node独创
流操作配合管道实现数据分段传输,常见的应用就是视频边下载边播放
数据的端到端传输会有生产者和消费者
生产和消费的过程往往存在等待,等待时候的数据存放在buffer中
Nodejs 中的Buffer是一片内存空间
Buffer的空间申请不是Node完成的,但是在使用层面上空间分配是由编写的JS代码控制的,空间回收的时候还是V8的GC算法管理和回收
总结
- 无须require的一个全局变量
- 实现Nodejs平台下的二进制数据操作
- 不占据V8堆内存大小的内存空间
- 内存的使用由Node控制,由V8的GC算法回收
- 一般配合Stream流操作,充当数据缓冲区(大文件的读写操作)
创建Buffer
Buffer是Nodejs的内置类
对应API就是类的实例方法或者是静态成员方法
创建Buffer实例
- alloc:创建指定字节大小的buffer
- allocUnsafe: 创建指定大小的buffer(不安全)
- from: 接收数据,创建buffer
代码演示
/* const b1 = Buffer.alloc(10)
const b2 = Buffer.allocUnsafe(10)
console.log(b1)
console.log(b2) */
// from
/* const b1 = Buffer.from('中')
console.log(b1) */ 二进制的中文中
// const b1 = Buffer.from([0xe4, 0xb8, 0xad])
/* const b1 = Buffer.from([0x60, 0b1001, 12])
console.log(b1)
console.log(b1.toString()) */ 转成字符串形式的中
/* const b1 = Buffer.from('中')
console.log(b1)
console.log(b1.toString()) */
const b1 = Buffer.alloc(3)
const b2 = Buffer.from(b1)
console.log(b1)
console.log(b2)
b1[0] = 1
console.log(b1)
console.log(b2)
Buffer实例方法
let buf = Buffer.alloc(6)
// fill 最多填充3位数据,第一参数是数值,第二是开始位置,第三是结束位置
/* buf.fill(123)
console.log(buf)
console.log(buf.toString()) */
// write
/* buf.write('123', 1, 4)
console.log(buf)
console.log(buf.toString()) */
// toString
/* buf = Buffer.from('buffer测试')
console.log(buf)
console.log(buf.toString('utf-8', 3, 9)) */
// slice
/* buf = Buffer.from('buffer测试')
let b1 = buf.slice(-3)
console.log(b1)
console.log(b1.toString()) */
// indexOf
/* buf = Buffer.from('zce爱前端,爱大家,我爱所有')
console.log(buf)
console.log(buf.indexOf('爱qc', 4)) */
// copy
let b1 = Buffer.alloc(6)
let b2 = Buffer.from('buffer测试')
b2.copy(b1, 3, 3, 6)
console.log(b1.toString())
console.log(b2.toString())
Buffer静态方法
代码演示
/* let b1 = Buffer.from('拉勾')
let b2 = Buffer.from('教育')
let b = Buffer.concat([b1, b2], 9)
console.log(b)
console.log(b.toString()) */
// isBuffer
let b1 = '123'
console.log(Buffer.isBuffer(b1))
自定义Buffer之split
Buffer的操作和数组有些类似,会有下标,length属性,但是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 = 'zce吃馒头,吃面条,我吃所有吃'
let bufArr = buf.split('吃')
console.log(bufArr)
FS模块
FS是内置核心模块,提供文件系统操作的API 包括文件的添加,删除,写入等 如果操作二进制,需要使用到BufferFS模块结构
- FS基本操作类
- FS常用API
权限位:在当前操作系统用户对于文件所具备的操作权限,权限分为r、w、s
标识符: Nodejs中flag表示对文件操作方式,是否可读,可写等
fd是操作系统分配给被打开文件的标识
操作文件API
文件读写和拷贝操作
同步写入和异步写入
const fs = require('fs')
const path = require('path')
// readFile
/* fs.readFile(path.resolve('data1.txt'), 'utf-8', (err, data) => {
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')
}
})
实现md转html
const fs = require('fs')
const path = require('path')
const marked = require('marked')
const browserSync = require('browser-sync')
/**
* 01 读取 md 和 css 内容
* 02 将上述读取出来的内容替换占位符,生成一个最终需要展的 Html 字符串
* 03 将上述的 Html 字符写入到指定的 Html 文件中
* 04 监听 md 文档内容的变经,然后更新 html 内容
* 05 使用 browser-sync 来实时显示 Html 内容
*/
let mdPath = path.join(__dirname, process.argv[2])
let cssPath = path.resolve('github.css')
let htmlPath = mdPath.replace(path.extname(mdPath), '.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 生成成功了')
})
})
})
}
})
browserSync.init({
browser: '',
server: __dirname,
watch: true,
index: path.basename(htmlPath)
})
const temp = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 1000px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 750px) {
.markdown-body {
padding: 15px;
}
}
{{style}}
</style>
</head>
<body>
<div class="markdown-body">
{{content}}
</div>
</body>
</html>
`
文件的打开和关闭
主要是针对大文件的读写,因为readFile等都是直接打开整个文件,大文件效果不友好
const fs = require('fs')
const path = require('path')
// open
/* fs.open(path.resolve('data.txt'), 'r', (err, fd) => {
console.log(fd)
}) */
// close
fs.open('data.txt', 'r', (err, fd) => {
console.log(fd)
fs.close(fd, err => {
console.log('关闭成功')
})
})
大文件读写操作
大文件需要边读边存,需要使用open等方法
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)
})
})
文件拷贝自定义实现
针对大文件的拷贝
const fs = require('fs')
/**
* 01 打开 a 文件,利用 read 将数据保存到 buffer 暂存起来
* 02 打开 b 文件,利用 write 将 buffer 中数据写入到 b 文件中
*/
let buf = Buffer.alloc(10)
// 01 打开指定的文件
/* fs.open('a.txt', 'r', (err, rfd) => {
// 03 打开 b 文件,用于执行数据写入操作
fs.open('b.txt', 'w', (err, wfd) => {
// 02 从打开的文件中读取数据
fs.read(rfd, buf, 0, 10, 0, (err, readBytes) => {
// 04 将 buffer 中的数据写入到 b.txt 当中
fs.write(wfd, buf, 0, 10, 0, (err, written) => {
console.log('写入成功')
})
})
})
}) */
// 02 数据的完全拷贝
/* fs.open('a.txt', 'r', (err, rfd) => {
fs.open('b.txt', 'a+', (err, wfd) => {
fs.read(rfd, buf, 0, 10, 0, (err, readBytes) => {
fs.write(wfd, buf, 0, 10, 0, (err, written) => {
fs.read(rfd, buf, 0, 5, 10, (err, readBytes) => {
fs.write(wfd, buf, 0, 5, 10, (err, written) => {
console.log('写入成功')
})
})
})
})
})
}) */
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()
})
})
目录操作API
const fs = require('fs')
// 一、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('删除成功')
}
}) */
目录创建的同步实现方式
const fs = require('fs')
const path = require('path')
/**
* 01 将来调用时需要接收类似于 a/b/c ,这样的路径,它们之间是采用 / 去行连接
* 02 利用 / 分割符将路径进行拆分,将每一项放入一个数组中进行管理 ['a', 'b', 'c']
* 03 对上述的数组进行遍历,我们需要拿到每一项,然后与前一项进行拼接 /
* 04 判断一个当前对拼接之后的路径是否具有可操作的权限,如果有则证明存在,否则的话就需要执行创建
*/
function makeDirSync (dirPath) {
let items = dirPath.split(path.sep)
for(let i = 1; i <= items.length; i++) {
let dir = items.slice(0, i).join(path.sep)
try {
fs.accessSync(dir)
} catch (err) {
fs.mkdirSync(dir)
}
}
}
makeDirSync('a\\b\\c')
目录创建的异步实现
const fs = require('fs')
const path = require('path')
const {promisify} = require('util')
/* function mkDir (dirPath, cb) {
let parts = dirPath.split('/')
let index = 1
function next () {
if (index > parts.length) return cb && cb()
let current = parts.slice(0, index++).join('/')
fs.access(current, (err) => {
if (err) {
fs.mkdir(current, next)
}else{
next()
}
})
}
next()
}
mkDir('a/b/c', () => {
console.log('创建成功')
}) */
// 将 access 与 mkdir 处理成 async... 风格
const access = promisify(fs.access)
const mkdir = promisify(fs.mkdir)
async function myMkdir (dirPath, cb) {
let parts = dirPath.split('/')
for(let index = 1; index <= parts.length; index++) {
let current = parts.slice(0, index).join('/')
try {
await access(current)
} catch (err) {
await mkdir(current)
}
}
cb && cb()
}
myMkdir('a/b/c', () => {
console.log('创建成功')
})
目录删除
const { dir } = require('console')
const fs = require('fs')
const path = require('path')
/**
* 需求:自定义一个函数,接收一个路径,然后执行删除
* 01 判断当前传入的路径是否为一个文件,直接删除当前文件即可
* 02 如果当前传入的是一个目录,我们需要继续读取目录中的内容,然后再执行删除操作
* 03 将删除行为定义成一个函数,然后通过递归的方式进行复用
* 04 将当前的名称拼接成在删除时可使用的路径
*/
function myRmdir (dirPath, cb) {
// 判断当前 dirPath 的类型
fs.stat(dirPath, (err, statObj) => {
if (statObj.isDirectory()) {
// 目录---> 继续读取
fs.readdir(dirPath, (err, files) => {
let dirs = files.map(item => {
return path.join(dirPath, item)
})
let index = 0
function next () {
if (index == dirs.length) return fs.rmdir(dirPath, cb)
let current = dirs[index++]
myRmdir(current, next)
}
next()
})
} else {
// 文件---> 直接删除
fs.unlink(dirPath, cb)
}
})
}
myRmdir('tmp', () => {
console.log('删除成功了')
})
模块化
为什么需要模块化?- 传统开发模式不能满足需求
- 组件化前端开发
- 代码冗余,无效请求
- 文件直接依赖关系复杂
- 命名冲突和污染,代码难以维护
常见的模块化规范
- Commonjs规范,nodejs使用的就是
- AMD规范,异步加载的模块,require.js是代表
- CMD规范,整合Common和AMD,代表是c.js
- ES Modules规范,就是常用的ES Module,导入导出
Commonjs规范
Commonjs出现是为了弥补Javascript语音没有模块化标准的缺陷,希望可以通过此标准使得浏览器作出一些改变,从而实现js代码到处运行,还能具备大型开发的应用
但是Commonjs主要应用Nodejs
Commonjs是语言层面上的规范,属于JS的超集
Commonjs规范
- 模块引用
- 模块定义
- 模块标识
Nodejs和CommonJS
- 任意一个文件就是一模块,具有独立作用域
- 使用require导入其他模块
- 将模块ID传入require实现目标模块定位
module属性
module.exports和exports的区别?
因为在CommonJS中只规定通过module.exports来执行数据的导出
单个exports是nodejs自己为了方便操作为每一个模块提供的变量,只是指向了module.exports对应的内存地址
require属性
Nodejs与CommonJS
使用module.exports与require实现模块的导入导出
module属性及其常见信息获取
exports导出数据及与module.exports区别
Commonjs规范下的模块同步加载
// 一、模块的导入与导出
/* const age = 18
const addFn = (x, y) => {
return x + y
}
module.exports = {
age: age,
addFn: addFn
} */
// 二、module
/* module.exports = 1111
console.log(module) */
// 三、exports
// exports.name = 'zce'
/* exports = {
name: 'syy',
age: 18
} */
// 四、同步加载
/* let name = 'lg'
let iTime = new Date()
while(new Date() -iTime < 4000) {}
module.exports = name
console.log('m.js被加载导入了') */
/* console.log(require.main == module) */
module.exports = 'lg'
// 一、导入
/* let obj = require('./m')
console.log(obj) */
// 二、module
// let obj = require('./m')
// 三、exports
/* let obj = require('./m')
console.log(obj) */
// 四、同步加载
/* let obj = require('./m')
console.log('01.js代码执行了') */
let obj = require('./m')
console.log(require.main == module)
模块分类及加载流程
模块分类- 内置模块:核心模块
- 文件模块
模块加载速度:
- 核心模块:Node源码编译时写入到二进制文件中
- 文件模块:需要经历整个加载就差,代码运行时,动态加载
加载流程
- 路径分析:以及标识符确定模块位置
- 文件定位:确定目标模块中具体的文件及文件类型
- 编译执行:采用对应的方式完成文件的编译执行
路径分析之标识符
- 路径标识符:基于标识符进行
- 非路径标识符:非路径常见于核心模块
文件定位
- 项目下存在m1.js模块,导入时候使用require(“m1”)语法
- m1.js -> m1.json -> m1.node
- 查找package.json文件,使用JSON.parse()解析
- main.js -> main.json -> main.node
- 将index作为目标模块中的具体文件名称
编译执行
- 将某个具体类型的文件安装响应的方式进行编译和执行
- node中每一个模块都是对象,创建新对象,按路径载入,完成编译执行
- JS文件执行:使用FS模块同步读入目标文件内容 -> 对内容进行语法包装,生成可执行JS函数 -> 调用函数时传入exports、module、require等属性值
- JSON文件:将读取到的文件内容通过JSON.parse()进行解析
编译时具有缓存加载原则,主要是提供模块加载速度,模块不存在,经历完整加载,模块加载完成,会使用路径作为索引进行缓存
事件模块
events与EventEmitter
常用API
使用
const EventEmitter = require('events')
const ev = new EventEmitter()
// on on的监听每一次都会触发
/* ev.on('事件1', () => {
console.log('事件1执行了---2')
})
ev.on('事件1', () => {
console.log('事件1执行了')
})
// emit
ev.emit('事件1')
ev.emit('事件1') */
// once once监听多个发布时只执行一次
/* ev.once('事件1', () => {
console.log('事件1执行了')
})
ev.once('事件1', () => {
console.log('事件1执行了--2')
})
ev.emit('事件1')
ev.emit('事件1') */
// off 是removeListener的别名,删除监听的
/* let cbFn = (...args) => {
console.log(args)
}
ev.on('事件1', cbFn) */
/* ev.emit('事件1')
ev.off('事件1', cbFn) */
// ev.emit('事件1', 1, 2, 3)
/* ev.on('事件1', function () {
console.log(this)
})
ev.on('事件1', function () {
console.log(2222)
})
ev.on('事件2', function () {
console.log(333)
})
ev.emit('事件1') */
const fs = require('fs')
const crt = fs.createReadStream()
crt.on('data')
node中会有eventloop,会自动管理事件流,触发异步回调
订阅发布模式
解决的问题?
解决没有promise时候回调嵌套问题
EventEmitter就是基于事件发布订阅实现的
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')
源码分析
核心就是有一个实例对象,通过实例对象调用API,来分析源码操作,就是事件的发布订阅
过程
- 先node执行看一下语法内容
- 进入调试,创建一份调试文件,修改skipFiles和program的目标路径
- 启动调试,进入event.js,会找到function EventEmitter中的init方法初始化
- init进入会一个判断,是否缓存了,没有进入创建对象
- 接下来找到addListener找到创建对象,内部根据函数名进行处理监听回调
launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
// "skipFiles": [
// "<node_internals>/**"
// ],
"program": "${workspaceFolder}\\05Module\\02-require-load.js"
}
]
}
const EventEmitter = require('events')
const ev = new EventEmitter()
ev.on('事件1', () => {
console.log('事件1执行了')
})
ev.on('事件1', () => {
console.log('2222')
})
ev.emit('事件1')
自定义Event
先构建一个类,在原型上添加on的回调,根据type类型决定缓存还是添加回调,emit方法通过遍历方式进行执行
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
微任务执行高于宏任务,典型的promise是微任务,setTimeout是宏任务
执行时候先进入微任务队列清空,清空之后进入宏任务队列调用,宏任务执行党的时候会将对应的微任务添加到微任务队列中执行,执行完之后继续下一个宏任务的执行
// setTimeout(() => {
// console.log('s1')
// Promise.resolve().then(() => {
// console.log('p1')
// })
// Promise.resolve().then(() => {
// console.log('p2')
// })
// })
// setTimeout(() => {
// console.log('s2')
// Promise.resolve().then(() => {
// console.log('p3')
// })
// Promise.resolve().then(() => {
// console.log('p4')
// })
// })
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
Nodejs的事件环
node.js事件循环机制
队列说明
Nodejs完整事件环
- 执行同步代码,将不同的任务添加至响应的队列
- 所有同步代码执行后回去执行满足条件微任务
- 所有微任务代码执行后会执行timer队列中满足的宏任务
- timer中的所有宏任务执行完成后就会依次切换队列
- 主要:在完成队列切换之前会先清空微任务代码
setTimeout(() => {
console.log('s1')
})
Promise.resolve().then(() => {
console.log('p1')
})
console.log('start')
process.nextTick(() => {
console.log('tick')
})
setImmediate(() => {
console.log('setimmediate')
})
console.log('end')
// start, end, tick, p1, s1, st
Nodejs事件环梳理
根据一段代码进行事件环的执行
浏览器平台执行是一个宏任务完成之后,会立马看一下微任务
node下事件队列的切换必须得等待当前队列里的任务全部执行完成之后才会发生的,并不会立刻执行p1,t1会继续s2,在加入p3,t2
setTimeout(() => {
console.log('s1')
Promise.resolve().then(() => {
console.log('p1')
})
process.nextTick(() => {
console.log('t1')
})
})
Promise.resolve().then(() => {
console.log('p2')
})
console.log('start')
setTimeout(() => {
console.log('s2')
Promise.resolve().then(() => {
console.log('p3')
})
process.nextTick(() => {
console.log('t2')
})
})
console.log('end')
// start, end, p2, s1, s2 , t1, t2, p1, p3
Nodejs与浏览器事件环区别
事件环不同
任务队列数
- 浏览器中资源二个任务队列
- Nodejs中由6个事件队列
微任务执行时机
- 二者都会在同步代码执行完毕后执行微任务
- 浏览器平台下每当一个宏任务执行完毕后就清空微任务
- Nodejs平台在事件队列切换时会去清空微任务
微任务优先级
- 浏览器事件环中,微任务存放事件队列,先进先出
- Nodejs中process.nextTick先于promise.then
** Nodejs事件环常见问题**
setTimeout可能执行时会有延迟,可能会先执行别的代码
// setTimeout(() => {
// console.log('timeout')
// }, 0)
// setImmediate(() => {
// console.log('immdieate')
// })
const fs = require('fs')
fs.readFile('./m1.js', () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immdieate')
})
})
核心模块之stream
Nodejs诞生之初就是为了提供IO性能,解决IO密集型问题
文件系统和网络模块实现了流接口、
Nodejs中的流就是处理流式数据的抽象接口
为什么应用程序使用流处理数据?
常见问题
- 同步读取资源文件,用户需要等待数据读取完成
- 资源文件最终一次性加载至内存,开销大
可以像是一个蓄水池,一点一点抽取数据,使得用户体验感好
流操作除了分段,还可以配合管道,去进行需求加工
glup的插件就是使用类似机制,接收到数据,进行处理在返回下一个插件
流处理数据优势
Nodejs中流的分类
Nodejs内置了stream,它实现了流操作对象
Nodejs流特点
- Stream模块实现了四个具体的抽象
- 所有流都基础自EventEmitter
简单示例
const fs = require('fs')
let rs = fs.createReadStream('./test.txt')
let ws = fs.createWriteStream('./test1.txt')
rs.pipe(ws)
可读流
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('出错了')
})
可写流
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('写入', () => {
console.log('ok1')
}) */
/* ws.on('open', (fd) => {
console.log('open', fd)
}) */
ws.write("2")
// close 是在数据写入操作全部完成之后再执行
/* ws.on('close', () => {
console.log('文件关闭了')
}) */
// end 执行之后就意味着数据写入操作完成
ws.end('写入')
// error
ws.on('error', (err) => {
console.log('出错了')
})
write方法
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('写入', () => {
console.log('ok1')
}) */
/* ws.on('open', (fd) => {
console.log('open', fd)
}) */
ws.write("2")
// close 是在数据写入操作全部完成之后再执行
/* ws.on('close', () => {
console.log('文件关闭了')
}) */
// end 执行之后就意味着数据写入操作完成
ws.end('写入')
// error
ws.on('error', (err) => {
console.log('出错了')
})
控制写入速度
按需完成限流,制定一个handworkmark,配合write返回方法使用
/**
* 需求:“控制写入” 写入指定的文件
* 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()
})
// pipe
背压机组
Node.js的stream已实现背压机制,
背压机制解决的问题:数据读写可能出现的问题,消费的速度可能超过生产的速度
可能出现内存溢出、GC频繁调用、其他进程变慢
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)
模拟文件可读流
在实现过程中,了解原理,更好的编写代码
监听open事件,拿到文件操作符fd
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.start
this.highWaterMark = options.highWaterMark || 64 * 1024
this.open()
}
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)
})
}
}
let rs = new MyFileReadStream('test.txt')
rs.on('open', (fd) => {
console.log('open', fd)
})
rs.on('error', (err) => {
console.log(err)
})
文件可读流生产数据,监听data事件,拿到数据
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.start
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)
fs.read(this.fd, buf, 0, this.highWaterMark, this.readOffset, (err, readBytes) => {
if (readBytes) {
this.readOffset += readBytes
this.emit('data', buf)
this.read()
} else {
this.emit('end')
this.close()
}
})
}
close() {
fs.close(this.fd, () => {
this.emit('close')
})
}
}
let rs = new MyFileReadStream('test.txt')
/*rs.on('open', (fd) => {
console.log('open', fd)
})
rs.on('error', (err) => {
console.log(err)
}) */
rs.on('data', (chunk) => {
console.log(chunk)
})
rs.on('close', () => {
console.log('close')
})
rs.on('end', () => {
console.log('end')
})
数据读取处理,
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)
})
链表结构
存储数据的结构
新版的Node中使用链表结构存储数据
为什么不使用数组存储数据?
链表是一系列节点的集合,约定每一个都是Node节点
每个节点都具有指向下一个节点的属性,将指向下一个节点的引用组合在一起,就形成链
链表分类
- 双向链表
- 单向链表
- 循环链表
单向链表
单向链表的实现
- 链表结构,链表的节点
实现链表结构,Node节点
/*
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
}
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++
}
}
const l1 = new LinkedList()
l1.add('node1')
console.log(l1)
- 实现单向链表结构及实例方法
/*
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--
}
}
const l1 = new LinkedList()
l1.add('node1')
l1.add('node2')
l1.add(1, 'node3')
l1.remove(1)
console.log(l1)
- 单向链表其他方法,修改set,获取get,删除clear
/*
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)
- 链表实现队列
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)
文件可写流实现
实现自己的write方法,明确write的并发处理
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()
}
open() {
// 原生 fs.open
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
this.emit('error', err)
}
// 正常打开文件
this.fd = fd
this.emit('open', fd)
})
}
}
const ws = new MyWriteStream('./f9.txt', {})
ws.on('open', (fd) => {
console.log('open---->', fd)
})
完善可写流,open拿到之后增加写入,写入参数和回调方法
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()
} else {
this.writing = true
// 当前不是正在写入那么就执行写入
this._write(chunk, encoding, cb)
}
return flag
}
_write(chunk, encoding, cb) {
console.log('正在执行第一次写入')
}
}
const ws = new MyWriteStream('./f9.txt', {
highWaterMark: 1
})
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')
})
_write方法实现
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
文件读写的拷贝行为,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)
// data