Node笔记

137 阅读12分钟

JavaScript模块化

1.CommonJS和Node

Node是CommonJS在服务器端一种实现;Browserify是CommonJS在浏览器中的一种实现;webpack打包工具提供对CommonJS的支持和转换。

在Node中每一个js文件都是一个单独的模块,该模块包括CommonJS规范的核心变量:exports、module.exports、require,可以使用这些变量进行模块化开发;exports和module.exports可以对模块中的内容进行导出; require函数可以导入其他模块(自定义模块、系统模块、第三方库模块)中的内容。

(1)exports

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出;

exports.name=name;
exports.sayHello=sayHello;

之后可以在另一个文件中导入:

// bar.js为文件名
const bar=require('./bar');

require通过各种查找方式,找到exports对象,将exports对象赋值给了bar变量;bar变量就是exports对象。(bar对象是exports对象的浅拷贝,即引用赋值)

(2)module.exports与exports的关系

CommonJS中是没有module.exports这个概念,但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module;所以在Node中真正用于导出的不是exports,而是module.exports。

module对象的exports属性是exports对象的一个引用,所以使用exports也可以导出。

(3)require(x)

X是一个核心模块,比如path、http,直接返回核心模块,并且停止查找;

X是以 ./ 或 ../ 或 /(根目录)开头的:

  • 将X当做一个文件在对应的目录下查找
  • 没有找到对应的文件,将X作为一个目录
  • 直接查找X,若没找到,则报错:not found

模块的加载过程:模块在被第一次引入时,模块中的js代码会被运行一次;模块被多次引入时会缓存,最终只加载一次,每个模块对象module都有一个loaded属性,为false表示未加载,为true表示已经加载。

如果存在循环引入的情况,加载顺序是什么?

可以用图结构来表示,图结构在遍历的过程中,有深度优先搜索和广度优先搜索,node采用深度优先算法。

2.CommonJS、AM、CMD以及ES6模块化

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。require.js采用了AMD规范:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。SeaJS使用CMD规范。

ES6 在语言标准的层面上,实现了模块功能,采用export和import关键字来实现模块化,它采用编译期的静态分析,并且也加入了动态引用的方式。

3.ES6模块化与CommonJS的区别

(1)ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

(2)CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

常用内置模块

1.path

(1)从路径中获取信息

  • dirname:获取文件的父文件夹;
  • basename:获取文件名;
  • extname:获取文件扩展名

(2)路径的拼接 : 不同操作系统使用的分隔符不同; 可以使用path.join函数拼接路径;

(3) 将文件和某个文件夹拼接,可以使用 path.resolve; resolve函数会判断拼接的路径前面是否有 /或../或./; 如果有表示是一个绝对路径,会返回对应的拼接路径;如果没有,那么会和当前执行文件所在的文件夹进行拼接

2.fs文件系统

1)同步操作文件,代码会被阻塞fs.statSync()
(2)异步回调函数操作文件
fs.stat("file",callback)
(3)异步Promise操作文件
fs.promises.stat("file").then(callback)

文件读写

(1)读取文件:fs.readFile(path[, options], callback) 
(2)给文件写入内容:fs.writeFile(file, data[, options], callback)
​
option参数是一个对象,flag属性表示写入方式,encoding表示字符编码

文件夹操作

1)新建文件夹
fs.mkdir()和fs.mkdirSync()
(2)文件重命名
fs.rename()

3.events模块

发出事件和监听事件都是通过EventEmitter类来完成的,都属于events对象。

  • emitter.on(eventName, listener)监听事件
  • emitter.off(eventName, listener)移除事件监听
  • emitter.emit(eventName[, ...args]):发出事件,可携带参数
  • emitter.eventNames():返回当前 EventEmitter对象注册的事件字符串数组
  • pemitter.getMaxListeners():返回当前 EventEmitter对象的最大监听器数量
  • pemitter.listenerCount(事件名称):返回当前 EventEmitter对象某一个事件名称,监听器的个数

npm install原理

1.有package.json 包管理文件才可以执行npm install

2.看是否有package-lock.json文件

3.没有package-lock.json文件,则构建依赖关系,然后从registry仓库下载安装包(压缩文件),将压缩包添加到本地的缓存文件中再将下载的压缩包解压到node_modules中,同时生成package-lock.json文件,完成安装

4.有package-lock.json文件,则先检查下载的包和package-lock.json文件中的包版本是否符合semver 版本规范,不一定完全一样,像 2.2.1 和 2.2.2 是一样的

5.不一致,则重新构建依赖关系,然后就是和第三部一样,去仓库下载,加压,添加缓存,解压到 node_modules中,生成新的package-lock.json

6.一致,则会去本地缓存文件中去查找缓存文件(通过查找算法),找到缓存文件,将压缩包解压到 node_modules中,完成安装

Buffer

文字、数字、图片、音频、视频最终都会使用二进制来表示。可以将Buffer看成一个存储二进制的数组; 数组中的每一项均保存8位二进制,即一个字节。

  1. Buffer和字符串
const buffer = Buffer.from(str)可将一个字符串放入到Buffer中
buffer.toString()可将Buffer转为字符串
  1. Buffer.alloc(8)可以创建一个8位长度的Buffer
  2. Buffer的内存分配机制

为了高效的使用申请来的内存,Node采用了slab分配机制。slab是一种动态的内存管理机制。 Node以8kb为界限来来区分Buffer为大对象还是小对象,如果是小于8kb就是小Buffer,大于8kb就是大Buffer。例如第一次分配一个1024字节的Buffer,Buffer.alloc(1024),那么这次分配就会用到一个slab,接着如果继续Buffer.alloc(1024),那么上一次用的slab的空间还没有用完,因为总共是8kb,1024+1024 = 2048个字节,没有8kb,所以就继续用这个slab给Buffer分配空间。如果超过8kb,那么直接用C++底层地宫的SlowBuffer来给Buffer对象提供空间。

node事件循环

  1. node运行机制

V8引擎解析JavaScript脚本。解析后的代码,调用Node API。libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。V8引擎再将结果返回给用户。

libuv维护了一个EventLoop和worker threads(线程池),EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等。

libuv的线程池(Thread Pool)会负责所有相关的操作,并且会通过轮训等方式等待结果;当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中;事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数。

  1. 阻塞和非阻塞,同步和异步的区别

(1)Libuv采用的就是非阻塞异步IO的调用方式;

(2)阻塞和非阻塞是对于被调用者来说的;在这里就是系统调用,操作系统提供了阻塞调用和非阻塞调用。

  • 阻塞式调用: 调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行。
  • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可

(3) 同步和异步是对于调用者来说的; 在这里就是自己的程序。

  • 同步调用:如果我们在发起调用之后,不会进行其他任何的操作,只是等待结果;
  • 异步调用:如果我们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行;
  1. Node事件循环的阶段

Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

(1)计时器阶段 (Timers) :初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。

(2)待定回调 (Pending callbacks) :处理上轮循环中的少数未执行的 I/O 回调(系统调用相关的回调)。

(3)Idle/Prepare:仅供node内部使用。

(4)Poll(轮询阶段)

  • 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。
  • 当回调队列为空时:但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。

(5)Check(检测阶段) :会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。

(6)关闭的回调函数(Close callbacks) :执行一些关闭回调,比如socket.on('close', ...)等。

以上所有阶段均不包含process.nextTick()

  1. Node的微任务和宏任务
  • 宏任务:setTimeout、setInterval、I/O事件、setImmediate、close事件;
  • 微任务:Promise的then回调、process.nextTick、queueMicrotask;
  1. setTimeout(回调函数, 0)、setImmediate(回调函数)执行顺序

(1)情况一:如果事件循环开启的时间(ms)是小于 setTimeout函数的执行时间的; 也就意味着先开启了event-loop,但是这个时候执行到timer阶段,并没有 定时器的回调被放到入 timer queue中;所以没有被执行,后续开启定时器和检测到有setImmediate时,就会跳过 poll阶段,向后继续执行;这个时候是先检测 setImmediate,第二次的tick中执行了timer中的 setTimeout;

(2)情况二:如果事件循环开启的时间(ms)是大于 setTimeout函数的执行时间的; 这就意味着在第一次 tick中,已经准备好了timer queue;所以会直接按照顺序执行即可;

  1. process.nextTick()

该函数独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,优先执行。

express框架

Express是一个路由和中间件的Web框架,Express应用程序本质上是一系列中间件函数的调用

  1. 中间件

中间件的本质是传递给express的一个回调函数,回调函数接受三个参数:request对象;response对象;next函数(在express中定义的用于执行下一个中间件的函数)。

中间件的作用:

  • 执行任何代码;
  • 更改请求和响应对象;
  • 结束请求-响应周期(返回数据);
  • 调用栈中的下一个中间件;

express有两种使用方式:app/router.use和app/router.methods;

  1. Express的路由
const userRouter=express.Router();

Koa框架

  1. koa注册的中间件提供了两个参数:

(1)ctx:上下文(Context)对象;koa没有将req和res分开,而是将它们作为 ctx的属性; ctx代表依次请求的上下文对象;ctx.request:获取请求对象;ctx.response:获取响应对象;

(2)next

  1. 中间件

koa通过创建的app对象,注册中间件只能通过use方法:没有提供methods的方式来注册中间件;也没有提供path中间件来匹配路径。可以使用request.path和request.method来区分不同路径和方法。

express内置了非常多好用的功能; koa是简洁的,只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制。在app中连最基本的get、post都没有给提供;需要通过自己或者路由来判断请求方式或者其他功能。