1.开始
Node.js 是一个开源和跨平台的 JavaScript 运行时环境。它几乎是任何类型项目的流行工具
Node.js 应用程序在单个进程中运行,无需为每个请求创建新的线程。Node.js 在其标准库中提供了一组异步的I/O原语,以防止 JavaScript 代码阻塞,通常,Node.js 中的库是使用非阻塞范式编写的
2.非阻塞
node 核心运行机制,node应用运行在单个进程中,node是单线程的(意思是node中只有一个线程用于处理 javascript),一个进程包含多个线程
3.事件循环
在一段代码中,JS代码并不总是按顺序同步执行
不管是在浏览器还是node中,为了不阻塞线程,很多情况下,一些代码是通过回调的方式去异步执行的,异步编程在node中是一种常态。
JS代码的执行顺序被打乱,那么就需要一种机制去协调各个事件的执行顺序,这种机制就是事件循环
异步API分类
- 定时器
- setTimeout
- setInterval
- I / O 操作
- 文件读写
- 数据库操作
- 网络请求
- ...
- node 独有
- process.nextTick (特殊)
- setImmediate
事件循环内部初始化了三个任务队列
- Timer 队列
- setTimeout
- setInterval
- Poll 队列
- 文件读写
- 数据库操作
- 网络请求
- ...
- 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 阶段继续等待
同步代码 -> 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
常用指令
7.模块系统
因为历史原因,node目前是同时存在两套模块系统
一个是 commonJS ,它是 node 默认使用的模块系统,目前大部分的 node 项目使用的都是 commonJS 规范
另一个是 ES module ,这是由官方定义的模块规范
commonJS 规范
- 每个 js 文件都是一个独立的模块,每个模块都有一个 module 对象用来记录模块的信息
- 通过 module.exports 或者 exports 可以导出模块
- 通过 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 规范
- 使用 export 导出模块
- 使用 import 导入模块
- 配置package.json的type字段,启用 ES module
区别
- commonJS: 运行时加载
- 嵌套关系:父 -> 子 -> 父
- ES module: 编译时加载
- 而 ES model 会在编译阶段将模块依赖解析成一个 有向图:子 -> 父
- 在 ESmodule 中 ,没有 _filename 和 dirname
- 在 commonJS 中, this指向当前模块,在ESmodule中,this指向undefined
- 在 ESmodule 中,引入模块需要传递完整的扩展名,而在 commonJS 的 require 函数可以省略
- ESmodule 默认运行在严格模式下
V8 引擎在执行一个脚本文件的时候,分为两个阶段
第一个阶段叫做预编译阶段,在这个阶段 js 引警会扫描整个脚本为变量分配内存空间,确定作用域链等等
第二个阶段才是代码的执行阶段
所谓运行时加载指的是 common js 的模块是在代码的执行阶段加载进来的
而 ES model是在预编译阶段加载进来的
也就是说 ES model 比 common js 更早地进行了加载
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 回调模式
- 当接口要支持多个事件的时候,最好使用事件模式,回调模式通常只处理单个事件
- 在语义上,事件模式关注的是某个操作是否发生了,回调模式关注的是某个操作是否成功了