Node 笔记
底层:
- V8: 执行js代码,提供桥梁接口
- Libuv: 事件循环,事件队列,异步IO
why
- 基于Reactor模式,单线程完成多线程任务
- Nodejs 更适用于IO密集型高并发请求
异步IO
如果是网络IO,则去调用操作系统对应的IO机制
- IO是应用程序的瓶颈所在
- 异步IO提高性能无需原地等待结果返回
- IO操作属于操作系统级别,平台都有对应实现
- Nodejs单线程配合事件驱动架构以及libuv实现了异步IO
事件驱动
事件驱动,发布订阅,观察者:
主体发布消息,其他实例接收消息
是Nodejs 实现高性能web服务的前提
事件多路分解器 => 调用操作系统层面IO接口 => 返回结果 => event queue => event loop
单线程
异步非阻塞IO配合事件回调通知来实现高并发
单线程:主线程是单线程,而不是nodejs只有单线程
libuv库中有一个线程池,可以通过各个线程来处理网络IO,文件IO等。
nodejs 劣势: 对于cpu密集型任务,会占用过多的cpu资源
应用场景
- BFF层
- 实时聊天程序
- IO密集型
- 前端工程化方面
实时API服务
全局对象
- __filename: 返回正在执行脚本文件的绝对路径
- __dirname: 返回正在执行脚本所在目录
- timer类函数:执行顺序与事件循环间的关系
- process: 提供与当前进程互动的接口
- require: 实现模块的加载
- module, exports: 处理模块导出
默认情况下,this是空对象,和global并不是一样的
在自执行函数中,是一样的
Process
- arrayBuffers: 一段独立的内存, 缓冲区的大小
- rss: 总的内存大小
- heapTotal: 堆内存大小
- heapUsed:已使用的堆内存大小
- external: c++插件和库使用的内存大小
cwd(): 运行目录
versions: node环境
arch: cpu架构
env.Path: 操作系统环境变量
env.USERPROFILE:当前系统管理员目录
运行状态
启动参数: argv
PID: pid
运行时间:uptime()
cpu
{user: 60000, system: 15000}
Process 2
事件驱动的编程,发布订阅的模式
fs
process.stdin
process.stdout
path
basename(): 获取路径中基础名称
dirname():获取路径中目录名称
extname(): 获取路径中扩展名称
isAbsolute(): 获取路径是否为绝对路径
join(): 拼接多个路径片段
resolve(): 返回绝对路径
paser(): 解析路径
format(): 序列化路径
normalize(): 规范化路径
Buffer
buffer 让 js 可以操作二进制
IO行为操作的就是二进制
流操作配合管道实现数据分段传输
数据的端到端传输会有生产者和消费者
nodejs 中 buffer 是一片内存空间
- 是全局变量
- 不占V8堆内存
- 一般配合 stream
创建Buffer
- alloc: 创建指定字节大小
- allocUnsafe: 创建指定字节大小(不安全)
- form: 接受数据,创建buffer
Buffer 实例方法
-
fill: 使用数据填充buffer
- 第二个参数:填充起始位置
- 第三个参数:填充截至位置
-
write: 向buffer中写入数据
- 有多少写多少,不会完全填充
- 第二个参数:写入起始位置
- 第三个参数:写入的长度
-
toString: 从buffer中提取数据
- 第一个参数: 字符集
- 第二个参数: 表示提取的起始位置
- 第三个参数: 表示提取的长度
-
slice: 截取buffer
- 负数代表从后往前截取
-
indexOf: 在buffer中查找数据
- 第二个参数:起始查找偏移量
-
copy: 拷贝buffer中的数据
- 第二个参数,
buffer静态方法
- concat: 将多个buffer 拼接成一个新的buffer
- isBuffer: 判断当前数据是否为buffer
Buffer.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).toString());
start = offset + 1;
}
ret.push(this.slice(start).toString());
return ret;
};
let str = Buffer.from("abcd");
console.log(str.split("b"));
fs
nodejs 中 flag表示对文件的读写权限
- r: 可读
- w: 可写
- s: 同步
- +: 执行相反操作
- x:表示排它操作
- a: 表示追加操作
- fd:操作系统分配给被打开文件的标识
文件操作
- readFile: 从指定文件中读取数据
- writeFile: 向指定文件中写入数据
- appendFIle: 追加的方式向指定文件中写入数据
- copyFIle: 将某个文件中的数据拷贝至另一文件
- watchFile: 对指定文件进行监控
md转html
文件打开与关闭
- open
- close
大文件读写操作
实现边读边存
目录操作
- access: 判断文件或目录是否有操作权限
- stat: 获取目录及文件信息
- mkdir:创建目录
- rmdir: 删除目录
- readdir: 读取目录中内容
- unlink: 删除指定文件
模块化历程
利用函数,对象,自执行函数实现分块
- commonjs:代码是同步运行的
commonjs
- 任意一个文件就是一个模块,具有独立作用域
- 使用require 导入其他模块
- 将模块ID传入require实现目标模块定位
module
- 任意js文件就是一个模块,可以直接使用module属性
- id: 返回模块标识符,一般是一个绝对路径
- filename: 返回文件模块的绝对路径
- loaded: 返回布尔值,表示模块是否完成加载
- parent: 返回对象存放调用当前模块的模块
- children: 返回数组,存放当前模块调用的其他模块
- exports: 返回当前模块需要暴露的内容
- paths: 返回数组,存放不同目录下的 node_modules位置
module.export 和 export 有什么区别
export是指向 module.export 的一个变量
不能单独赋值
require
- 读入并且执行一个模块文件
- resolve: 返回模块文件绝对路径
- extensions: 依据不同后缀名执行解析操作
- main: 返回主模块对象
commonjs
- 模块加载是同步的
- require.main === module, 判断是否是主模块
模块分类及加载流程
- 内置模块
- 文件模块
模块加载速度
- 核心模块:Node源码编译时写入到二进制文件中
- 文件模块:代码运行时,动态加载
加载流程
-
路径分析:依据标识符确定模块位置
- 标识符路径
- 非标识符路径
-
文件定位:确定目标模块中具体的文件及文件类型
-
编译执行:采用对应的方式完成文件的编译执行
- 使用fs模块同步读入目标文件内容
- 对内容进行语法包装,生产可执行js函数
- 调用函数时传入 exports,modules,require等属性值
-
JSON
- 读取到的内容通过JSON.parse解析即可
缓存优化原则
- 提高模块加载速度
- 当前模块不存在,则经历一次完整加载流程
- 模块加载完成后,使用路径作为索引进行缓存
VM
创建独立运行的沙箱模块
EventEmitter
通过EventEmitter类实现事件统一管理
- on: 添加当事件被触发时调用的回调函数
- emit: 触发事件,按照注册的顺序同步调用每个事件监听器
- once: 添加只执行一次的事件回调函数
- off: 移除事件监听器
发布订阅模式
定义对象间一对多的依赖关系
解决什么问题:
发布订阅要素
- 缓存队列,存放订阅者信息
- 具有增加,删除订阅的能力
- 状态改变时通知所有订阅者执行监听
观察者模式中没有消息调度中心
状态发生改变时,发布订阅无须主动通知
浏览器事件循环
每当一个宏任务执行完成后,都会去清空当前微任务队列
Node事件循环浏览器事件区别
- 任务队列数不同
- Nodejs 微任务执行时机不同
- 微任务优先级不同
浏览器
- 只有两个任务队列
- node有6个任务队列
微任务执行时机
- 二者都会在同步代码执行完毕后执行微任务
- 浏览器平台下每当一个宏任务执行完毕之后就会清空微任务
- nodejs 平台在事件队列切换时就会去清空微任务
微任务优先级
- 浏览器中,微任务存放于事件队列,先进先出
- nodejs 中 process.nextTick先于 所有微任务
nodejs 事件循环常见问题
流
流就是处理流式数据的抽象接口
流处理数据的优势
- 时间效率: 流的分段处理可以同时操作多个数据chunk
- 空间效率: 同一时间流无需占据大内存空间
- 使用方便: 流配合管理,扩展程序变得简单
流的分类
- Readable: 可读流,能够实现数据的读取
- Writeable: 可写流,能够实现数据的写操作
- Duplex: 双工流,既可读又可写
- Tranform: 转换流,可读可写,还能实现数据转换
Nodejs流特点
- Stream 模块实现了四个具体的抽象
- 所有流都继承自 EventEmitter
stream 可读流
流动模式,暂停模式
区别就是消费数据的时候是否需要主动去调用 read 方法
消费数据
- readable事件:当流中存在可读数据时触发
- data事件:当流中数据块传给消费者后触发
- end: 读取结束
总结
- 明确数据生产与消费流程
- 利用API实现自定义的可读流
- 明确数据消费的事件使用
const { Readable } = require("stream");
const arr = ["111", "222", "333"];
class myReadable extends Readable {
constructor(source) {
super();
this.source = source;
}
_read() {
const data = this.source.shift() || null;
this.push(data);
}
}
const rd = new myReadable(arr);
// rd.on("readable", () => {
// let data = null;
// while ((data = rd.read(3)) !== null) {
// console.log(data.toString());
// }
// });
rd.on("data", (chunk) => {
console.log(chunk.toString());
});
Write执行流程
关于
highWaterMark的几个点1.
- 第一次调用 write方法时,是将数据写入到文件中
- 第二次调用 write方法时,就是将数据写入到缓存中
- 生产速度和消费速度是不一样的,一般情况下,生产速度比消费速度快很多
- 当 flag 为 false 之后,并不意味着当前次的数据不能被写入了,但是我们应该告知数据的生产者,当前的消费速度已经跟不上生产速度了,所以这个时候,一般我们会将可读流的模块修改为暂停模式。
- 当数据生产者暂停之后,消费者会慢慢的把消化它内部缓存中的数据,直到可以再次被执行写入操作
- 当缓冲区可以继续写入数据的时候如何让生产者直到呢?、
- drain 事件
const fs = require("fs");
const ws = fs.createWriteStream("text.txt", {
highWaterMark: 3,
});
let flag = ws.write("1");
console.log(flag);
flag = ws.write("2");
console.log(flag);
flag = ws.write("3");
console.log(flag);
drain与写入速度
const fs = require("fs");
const ws = fs.createWriteStream("text.txt", {
highWaterMark: 3,
});
let arr = "拉钩教育".split("");
let num = 0;
let flag = true;
function writes() {
flag = true;
while (num !== 4 && flag) {
flag = ws.write(arr[num]);
num++;
}
}
writes();
ws.on("drain", () => {
console.log(123);
writes();
});
背压机制
Nodejs 的 stream已经实现了背压机制
问题: 简单的生产消费会出现:内存溢出,GC频繁调用,其他进程变慢
链表
数组缺点
- 数组存储数据的长度具有上限
- 数组存在塌陷问题
链表是一系列节点的集合,每个节点都具有指向下一个节点的指向
网络通信
Mac地址,是一台主机的标识
局域网存在大量主机会造成广播风暴
为什么是四次挥手
因为 一个服务端会服务于多个客户端
我们不能保证某一个客户端将数据发送到服务端后
服务端就能立即将数据全部回传给客户端
所以挥手必须为四次,第三次是服务端回传数据