Skip to main content

Node.js学习

· 12 min read
LIU

1.开始

Node.js 是一个开源和跨平台的 JavaScript 运行时环境。它几乎是任何类型项目的流行工具

Node.js 应用程序在单个进程中运行,无需为每个请求创建新的线程。Node.js 在其标准库中提供了一组异步的I/O原语,以防止 JavaScript 代码阻塞,通常,Node.js 中的库是使用非阻塞范式编写的

2.非阻塞

node 核心运行机制,node应用运行在单个进程中,node是单线程的(意思是node中只有一个线程用于处理 javascript),一个进程包含多个线程

image-20220613120754407

image-20220613120832956

image-20220613120952941

3.事件循环

在一段代码中,JS代码并不总是按顺序同步执行

不管是在浏览器还是node中,为了不阻塞线程,很多情况下,一些代码是通过回调的方式去异步执行的,异步编程在node中是一种常态。

JS代码的执行顺序被打乱,那么就需要一种机制去协调各个事件的执行顺序,这种机制就是事件循环

异步API分类

  • 定时器
    • setTimeout
    • setInterval
  • I / O 操作
    • 文件读写
    • 数据库操作
    • 网络请求
    • ...
  • node 独有
    • process.nextTick (特殊)
    • setImmediate

事件循环内部初始化了三个任务队列

  1. Timer 队列
    • setTimeout
    • setInterval
  2. Poll 队列
    • 文件读写
    • 数据库操作
    • 网络请求
    • ...
  3. Check 队列
    • setImmediate

以下代码,I / O 操作回调在 Poll 阶段被执行,setTimout 进入 Timer 队列,setImmediate 进入 Check 队列,此时事件循环处于 Poll 阶段,然后它会接着向下执行,调用 Check 队列里边的回调函数,然后再调用 Timer 队列里的回调,这样 setImmediate 总会被优先调用

fs.readFile(filename,()=>{
setTimout(()=>{
console.log('timeout');
},0)
setImmediate(()=>{
console.log('setImmediate')
})
})

process.nextTick

不属于事件循环的一部分,在异步模块里面有 nextTick 队列,这个队列的优先级比事件循环更高

Node 事件循环中 从 Timer 到 Check 运行一周,称为一个 Tick

process.nextTick 的作用就是将包裹的函数插入到每个 Tick 的头部,这样 nextTick 总会在先于下个 Tick 运行之前执行

nextTick 的执行 会在同步代码之后,事件循环之前

微任务

Promise 回调,在 nextTick 下面,事件循环前面

参考图

事件循环到 Poll 阶段会做判断,如果 Timer 和 Check 队列都为空,事件循环会在这里阻塞

而如果此时 Check 队列有一个回调需要执行,所以事件循环会继续执行,将 Check 队列里的回调推入到调用栈当中,然后回到 Poll 阶段继续等待

image-20220612195118710

同步代码 -> nextTick -> 微任务 -> 事件循环

4.异常处理

node 应用程序运行在一个单进程,单线程环境当中,这就意味着只要出现一个错误,整个服务器都会崩溃

同步代码

使用 try...catch

异步代码

  • promise 使用 .catch
  • async / await 使用 try...catch

程序中所有未被捕获的异常

process.on('uncaughtException') (兜底方案,实用性较差)

5.异步编程与流程控制

在 node 中,异步编程是一种常态

在 node 中 所有回调函数都遵循 错误优先 的风格

三个阶段:回调函数 -> promise -> async / await

6.npm

生产依赖

dependencies:npm install

开发依赖

devDependencies:npm install - -save -dev

常用指令

image-20220613143038775

7.模块系统

因为历史原因,node目前是同时存在两套模块系统

一个是 commonJS ,它是 node 默认使用的模块系统,目前大部分的 node 项目使用的都是 commonJS 规范

另一个是 ES module ,这是由官方定义的模块规范

commonJS 规范

  1. 每个 js 文件都是一个独立的模块,每个模块都有一个 module 对象用来记录模块的信息
  2. 通过 module.exports 或者 exports 可以导出模块
  3. 通过 require() 函数可以导入模块
// 不是全局变量,它只存在于当前模块的作用域,每个模块访问的并不是同一个 model 对象
console.log(model)
// 当前模块的绝对路径
console.log(filename)
// 当前模块所在的目录
console.log(dirname)

const str ="我是a文件的字符串"
const foo = () => {console. log ("我是a文件的函数")}

// 导出
module.exports = {
str,
foo
}
// 或者, 不能混用,否则 module.exports 会覆盖 exports
exports.str = str;
exports.foo = foo;

console.log(module.exports === exports) // true

// 导入
const {str,foo}= require("./a.js")

核心模块和第三方模块可以直接使用模块名进行加载,而我们自己实现的自定义模块必须传入具体的模块路径

  • 核心模块:核心模块随着node一起安装,不需要额外的安装,可以直接引用
  • 第三方模块:需要npm安装的模块,安装位置是node_modules
  • 自定义模块:我们自己定义的模块,引用是需要写路径

ES module 规范

  1. 使用 export 导出模块
  2. 使用 import 导入模块
  3. 配置package.json的type字段,启用 ES module

区别

  • commonJS: 运行时加载
    • 嵌套关系:父 -> 子 -> 父
  • ES module: 编译时加载
    • 而 ES model 会在编译阶段将模块依赖解析成一个 有向图:子 -> 父
  • 在 ESmodule 中 ,没有 _filename 和 dirname
  1. 在 commonJS 中, this指向当前模块,在ESmodule中,this指向undefined
  2. 在 ESmodule 中,引入模块需要传递完整的扩展名,而在 commonJS 的 require 函数可以省略
  3. ESmodule 默认运行在严格模式下

V8 引擎在执行一个脚本文件的时候,分为两个阶段

第一个阶段叫做预编译阶段,在这个阶段 js 引警会扫描整个脚本为变量分配内存空间,确定作用域链等等

第二个阶段才是代码的执行阶段

所谓运行时加载指的是 common js 的模块是在代码的执行阶段加载进来的

而 ES model是在预编译阶段加载进来的

也就是说 ES model 比 common js 更早地进行了加载

image-20220613001230496

8.buffer

buffer 二进制数据的缓存区

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像 TCP 流、文件流、视频图片时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

9.stream 流

stream 流 是node的一个核心模块,也是一种编程模式

客观上说,我们在 node 开发中一般不会直接使用 stream 这个模块,这个模块更接近底层

我们平时使用的都是对stream的二次封装,比如 http 模块里的 req 和 res 其实都是流对象

还有在 fs 模块,我们可以使用 createReadStream 和 createReadStream 方法把文件转化为流对象

还有 sleep 模块和 crypto 模块,这是对转化流的典型应用

stream 主要的应用场景就是 io 操作,像网络请求、文件处理都属于 io 操作,它的作用就是用来处理端到端的数据交换的

在 node 中对数据的处理比较传统的模式是使用缓冲,在这种模式下,程序要把需要处理的资源从磁盘全部加载到内存缓存区

10.事件模式

Events 事件,这个模块也属于 node 的一个基础模块,很多其他模块都继承了 event 模块的 EventEmitter

比如上节的 stream 就是一个 EventEmitter 的实例

它的作用主要是在 node 环境中,提供一种函数调用的模式,这种模式属于观察者模式

在这种模式下,我们可以给事件注册一个或者多个监听器,当事件发生的时候,这些监听器就会执行 EventEmitter 几个核心 api 方法

EventEmitter

  • on:用于注册监听器
  • once:注册一次性监听器
  • emit:触发事件,同步地调用监听器
    • 有多个(on方法)监听器,emit 方法会同步的调用数组里的每一个监听器,所以监听器的注册顺序就是它的执行顺序
  • removeListener 方法,移除某个事件的监听器

事件模式 VS 回调模式

  • 当接口要支持多个事件的时候,最好使用事件模式,回调模式通常只处理单个事件
  • 在语义上,事件模式关注的是某个操作是否发生了,回调模式关注的是某个操作是否成功了