Node 笔记

96 阅读10分钟

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 方法

image-20220603170614432

消费数据

  • 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.

  1. 第一次调用 write方法时,是将数据写入到文件中
  2. 第二次调用 write方法时,就是将数据写入到缓存中
  3. 生产速度和消费速度是不一样的,一般情况下,生产速度比消费速度快很多
  4. 当 flag 为 false 之后,并不意味着当前次的数据不能被写入了,但是我们应该告知数据的生产者,当前的消费速度已经跟不上生产速度了,所以这个时候,一般我们会将可读流的模块修改为暂停模式。
  5. 当数据生产者暂停之后,消费者会慢慢的把消化它内部缓存中的数据,直到可以再次被执行写入操作
  6. 当缓冲区可以继续写入数据的时候如何让生产者直到呢?、
  7. 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频繁调用,其他进程变慢

image-20220604141825658

链表

数组缺点

  • 数组存储数据的长度具有上限
  • 数组存在塌陷问题

链表是一系列节点的集合,每个节点都具有指向下一个节点的指向

网络通信

Mac地址,是一台主机的标识

局域网存在大量主机会造成广播风暴

为什么是四次挥手

因为 一个服务端会服务于多个客户端

我们不能保证某一个客户端将数据发送到服务端后

服务端就能立即将数据全部回传给客户端

所以挥手必须为四次,第三次是服务端回传数据