Node.js核心模块
内置模块之 PATH(用于处理文件/目录的路径)
path模块提供了一些实用工具,用于处理文件和目录的路径
- basename() 获取路径中基础名称
- 返回的就是接收路径当中的最后一部分
- 第二个参数表示扩展名,如果说没有设置则返回完整的文件名称带后缀
- 第二个参数作为后缀时,如果没有在当前路径中被匹配到,那么就被会忽略
- 处理目录路径的时候如果说,结尾处有路径分隔符,则也会被忽略掉
-
dirname() 获取路径中目录名称
-
extname() 获取路径中扩展名称
-
isAbsolute() 获取路径是否为绝对路径
-
join() 拼接多个路径片段
-
resolve() 返回绝对路径
-
parse() 解析路径
-
format() 序列化路径
-
normalize() 规范化路径
Buffer 对象用于表示固定长度的字节序列。
+ 无须require的一个全局变量
+ 实现nodejs 平台下的二进制数据操作
+ 不占据V8堆内存大小的内存空间
Fs 文件操作系统
+ fs是Nodejs中内置核心模块
+ 代码层面上fs分为基本操作类和常用api
+ 权限位、标识符、操作符
- 常见 flag 操作符
- r:表示可读
- w:表示可写
- s:表示同步
- +:表示执行相反操作
- x:表示排它操作
- a:表示追加操作
文件的打开与操作
-
fs.open(path, flags, [mode], [callback(err,fd)])
- path 文件路径
- flags 可以是以下的值
- ‘r’ - 以读取模式打开文件
- ‘r+’ - 以读写模式打开文件
- ‘rs’ - 使用同步模式打开并读取文件。指示操作系统忽略本地文件系统缓存
- ‘rs+’ - 以同步的方式打开,读取 并 写入文件
- 注意:这不是让fs.open变成同步模式的阻塞操作。如果想要同步模式请使用fs.openSync()。
- ‘w’ - 以读取模式打开文件,如果文件不存在则创建
‘wx’ - 和 ’ w ’ 模式一样,如果文件存在则返回失败
‘w+’ - 以读写模式打开文件,如果文件不存在则创建
‘wx+’ - 和 ’ w+ ’ 模式一样,如果文件存在则返回失败 - ‘a’ - 以追加模式打开文件,如果文件不存在则创建
‘ax’ - 和 ’ a ’ 模式一样,如果文件存在则返回失败
‘a+’ - 以读取追加模式打开文件,如果文件不存在则创建
‘ax+’ - 和 ’ a+ ’ 模式一样,如果文件存在则返回失败
-
mode 用于创建文件时给文件制定权限,默认0666
-
callback 回调函数会传递一个文件描述符 fd ,和一个异常err
大文件读写操作
A文件要把内容复制到B文件里, 需要一个内存来暂存内容,边读边写,可以用Buffer来实现,避免直接复制过去造成内存的溢出。
const fs = require('fs')
// read : 所谓的读操作就是将数据从磁盘文件中写入到 buffer 中
let buff = Buffer.alloc(10)
/**
* fd 定位当前被打开的文件
* rfd 文件标识符
* buf 用于表示当前缓冲区
* offset 表示当前从 buf 的哪个位置开始执行写入
* length 表示当前写入的长度
* position 表示当前从文件的那个位置开始读取
*/
fs.open('data.txt', 'r', (err, rfd) => {
console.log(rfd); // 3
fs.read(rfd, buff, 0, 4, 3, (err, readBytes, data) => {
console.log(readBytes); // 4
console.log(data); // <Buffer 34 35 36 37 00 00 00 00 00 00>
console.log(data.toString()); // 4567
})
})
buf = Buffer.from('1234567890')
fs.open('b1.txt', 'w', (err, wfd) => {
/**
* wfd 文件标识符
* buf 存放数据的缓存区
* 1 第一个位置是从 buffer 的哪个位置取数据
* 4 表示具体写多少个长度
* 0 表示从文件的哪个位置执行写操作,一版从0开始执行写操作
*/
fs.write(wfd, buf, 1, 4, 0, (err, written, buffer) => {
console.log(written, '----'); // 4 ----
fs.close(wfd) // 记得关闭文件,减少内存的占用
})
})
// 会生成一个 b1.txt 文件,内容是 2345
文件拷贝自定义实现
const fs = require('fs')
/**
* 01 打开 a 文件,利用 read将数据保存在 buffer 暂存起来
* 02 打开 b 文件,利用 write 将 buffer 中数据写入到 b 文件中
*/
let buf = Buffer.alloc(10)
// 01 打开制定的文件 拷贝10个字节
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, writter) =>{
console.log('写入成功');
})
})
})
})
// 02 数据的完全拷贝
// fs.open('a.txt', 'r', (err, rfd) => {
// fs.open('b.txt', 'w', (err, wfd) => {
// fs.read(rfd, buf, 0, 10, 0, (err, readBytes) => {
// fs.write(wfd, buf, 0, 10, 0, (err, writter) =>{
// fs.read(rfd, buf, 0, 5, 10, (err, readBytes) => {
// fs.write(wfd, buf, 0, 5, 10, (err,writter) => {
// 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, writter) => {
next()
})
})
}
next()
})
})
// 生成一个 b.txt 文件,内容为A的内容
FS 之目录操作 API
常见目录操作 API
-
access:判断文件或目录是否具有操作权限
-
stat:获取目录及文件信息
-
mkdir:创建目录
-
rmdir:删除目录
-
readdir:读取目录中内容
-
unlink:删除指定文件
创建目录之同步实现
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')
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('创建成功');
})
目录删除之异步实现
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与CommonJs
+ 任意一个文件就是一模块,具有独立作用域
+ 使用require导入其它模块
+ 将模块ID传入require实现目标模块定位
模块分类及加载流程
-
模块分类
-
内置模块
-
文件模块
-
-
模块加载速度
-
核心模块:Node 源码编译时写入到二进制和文件中
-
文件模块:代码运行时,动态加载
-
-
加载流程
-
路径分析:依据标识符确定模块位置
-
文件定位:确定目标模块中具体的文件及文件类型
-
编译执行:采用对应的方式完成文件的编译执行
-
-
路径分析之标识符
-
路径标识符
-
非路径标识符(非路径常见于核心模块)
-
-
文件定位
-
编译执行
-
将某个具体类型的文件按照相应的方式进行编译和执行
-
创建新对象,按路径载入,完成编译执行
-
-
JS文件的编译执行
- 使用fs模块同步读入目标文件内容
- 对内容进行语法包装,生成可执行JS函数
- 调用函数时传入exports、module、require等属性值
-
JSON文件编译执行
- 将读取到的内容通过JSON.parse()进行解析
-
缓存优化原则
- 提高模块加载速度
- 当前模块不存在,则经历一次完整加载流程
- 模块加载完成后,使用路径作为索引进行缓存
-
加载流程小结
-
路径分析:确定目标模块位置
-
文件定位:确定目标模块中的具体文件
-
编译执行:对模块内容进行编译,返回可用 exports 对象
-
核心模块之 Events
-
通过 EventEmitter 类实现事件统一管理
-
events 与 EventEmitter
-
node.js 是基于时间驱动的异步操作架构,内置 events 模块
-
events 模块提供了 EventEmitter 类
-
node.js 中很多内置核心模块继承 EventEmitter
-
-
EventEmitter 常见 API
-
on:添加当事件被触发时调用的回调函数
-
emit:触发事件,按照注册的序同步调用每个事件监听器
-
once:添加当事件在注册之后首次被触发时调用的回调函数
-
off:移除特定的监听器
-
发布订阅
-
定义对象间一对多的关系
-
发布订阅要素
-
缓存队列,存放订阅者信息
-
具有增加、删除订阅的能力
-
状态改变时通知所有订阅者执行监听
-
-
发布订阅中存在调度中心
-
状态发生改变时,发布订阅者无须主动通知
-
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)
// 事件1执行了 [ 1, 2 ]
// 事件1---2
// 事件1执行了 [ 1, 2 ]
// ev.on('事件1', fn)
// ev.emit('事件1', '前')
// ev.off('事件1', fn)
// ev.emit('事件1', '后')
// 事件1执行了 [ '前' ]
ev.once('事件1', fn)
ev.off('事件1', fn)
// ev.emit('事件1', '前')
ev.emit('事件1', '后')
// 1
// 事件1执行了 [ '前' ]
浏览器中的 Eventloop 事件环
完整事件环执行顺序
- 从上至下执行所有的同步代码
- 执行过程中将遇到的宏任务与微任务添加至相应的队列
- 同步代码执行完毕后,执行满足条件的微任务回调
- 微任务队列执行完毕后执行所有满足需求的宏任务回调
- 循环事件环操作
*注意:每执行一个宏任务之后就会立刻检查微任务对列*
Nodejs 下的事件环
-
队列说明:从上至下执行
-
timers:执行 setTimeout 与 setInterval 回调
-
pengding callbacks:执行系统操作的回调,例如tcp udp
-
idle,prepare:只在系统内部进行使用
-
poll:执行与 I/O 相关的回调
-
check:执行 setImmediate 中的回调
-
close callbacks:执行 close 事件的回调
-
-
Nodejs 完整事件环
-
执行同步代码,将不同的任务添加至相应的队列
-
所有同步代码执行后回去执行满足条件微任务
-
timer 中的所有宏任务执行完成后就会依次切换队列
-
Nodejs与浏览器事件环区别
-
任务队列数不同
-
浏览器中只有二个任务队列
-
Nodejs 中有6个事件队列
-
-
Nodejs 微任务执行实际不同
-
二者都会在同步代码执行完毕后执行微任务
-
浏览器平台下每当一个宏任务执行完毕后就清空微任务
-
Nodejs 平台在事件队列切换时会去清空微任务
-
-
微任务优先级不同
-
浏览器事件环中,微任务存放于事件队列,先进先出
-
Nodejs 中 process.nextTick 先于 promise.then
-
Nodejs事件环常见问题
- setTimeout 和 setImmediate同步执行顺序是随机的
原因:是因为 setTimeout后面要跟一个时间,没写的情况下默认是0,不管是在Node和浏览器的平台下,都会有一个不稳定的因素,有些情况下会产生一些延时,延时的话,代码从上往下,会先加载 setImmediate,后加载 setTimeout;没有延时,就是先加载 setTimeout,后加载 setImmediate
解决办法:放在 I/O 回调函数,就不会出现这样的情况
核心模块之stream
-
ls|grep *.js
将左边的数据写到右边 js 文件中 -
Node.js 诞生之初就是为了提高 IO 性能
-
文件操作系统和网络模块实现了流接口
-
Node.js 中的流就是处理流式数据的抽象接口
-
应用程序中为什么使用流来处理数据?
-
原来方式常见问题:
-
同步读取资源文件,用户需要等待数据读取完成
-
资源文件最终一次性加载至内存,开销较大
-
-
现在方式
-
流处理数据的优势
-
时间效率:流的分段处理可以同时操作多个数据 chunk
-
空间效率:同一时间流无须占据大内存空间
-
使用方便:流配合管理,扩展程序变得简单,
-
-
-
-
Node.js 内置了 stream,它实现了流操作对象
-
Node.js 中流的分类
-
Readable:可读流,能够实现数据的读取
-
Writeable:可写流,能够实现数据的写操作
-
Duplex:双工流,即可读又可写
-
Tranform:转换流,可读可写,还能实现数据转换
-
-
Node.js 流特点
-
Stream 模块实现了四个具体的抽象
-
所有流都继承自 EventEmitter
-