这段时间在学习 Node,看书、看文档资料的时候概念感觉太乱了,于是打算写一篇文章来整理自己学到的知识,就有了这篇文章)

# Node 简介

Node.js 是一个开源、跨平台的 JavaScript 运行时环境。2009 年的时候,Ryan Dahl 想开发一个高性能的 web 服务器,经过一番深思熟虑,他最终选择了 JavaScript 作为 Node 的实现语言。时至今日,Node 已经成为最火热的技术之一。

# Node 的特点

  • 事件驱动

    和浏览器端 JavaScript 类似,

  • 异步 I/O

  • 单线程

    准确来说是 JavaScript 的执行是单线程的,其实 Node 中不同平台内部完成 I/O 任务另有线程池

    但其实类似前端浏览器中的 Web Workers,Node 也提供了创建子进程的方法来高效的利用 CPU 和 I/O

  • 跨平台

    Node 基于 libuv 实现跨平台(libuv 是一个跨平台的异步 I/O 库,它提供的内容不仅是 I/O,还有进程、线程、定时器、线程池等。)

    Node 用 libuv 作为抽象封装层,使得所有平台兼容性的判断由该层来完成。Node 在编译时会判断平台条件,选择性编译 unix 目录或 win 目录下的源文件到目标程序中

# Node 与浏览器上的 JavaScript 不同之处

  • 面向范围不同

    浏览器上的 JavaScript 是用于前端开发的,而 Node 是用于 Web 服务器开发的

  • 语言组成不同

    在前端中,JavaScript = ECMAScript + DOM + BOM

    Node 中的语言(按我的理解)是等于 ECMAScript + 和操作系统交互的 API(诸如读取文件等)

  • 运行环境不同

    前端的 JavaScript 跑在浏览器中的 js 引擎 (不同浏览器的 js 引擎不同,如最经典的 Chrome 的引擎是 V8,除此之外还有 SpiderMonkey、Nitro 等),是被限制在浏览器中的沙箱的。

    Node.js 在浏览器外运行 V8(Chrome 的 JavaScript 引擎)。基于 V8 的执行效率,Node 的计算能力也是比较优秀的。

# Node 应用场景

显然,异步 I/O 的使用场景最大就是 I/O 密集型场景。它的优势主要在于 Node 利用事件循环的处理能力。

# Node(零碎)基础知识

# 从命令行接收输入

利用 readline 模块创建一个接口从 process.stdin 读取

const readline = require('readline').createInterface({
  input:process.stdin,
  output:process.stdout
});
readline.question('what\'s your name? \n',name => {
  console.log(`hello! ${name}!`);
  readline.close();
});

# Node 模块规范

Node 中模块规范用的是 CommonJS 规范,它的使用方式大致如下:

// 模块引用
const http = require('http');
// 导出一
exports.add = (a,b) => a+b;
// 导出二
module.exports = {
  ...
};

它的特点有以下几个:

  1. CommonJS 以同步的方式加载模块。这一点其实可以理解,因为 Node 通常是运行于服务端,而在服务端模块文件通常存放在本地磁盘,读取速度比起前端通过网络下载要快得多,所以一般没有什么问题(但是因此也不适用于前端)
  2. CommonJS 输出的是模块的拷贝,这一点与 ESNext 模块不同。模块一旦输出后便独立(即后续更改不影响)
  3. Node 中对引入过的模块都会进行缓存,减少后续引入的开销。

# 核心模块

  1. util,提供常用函数的集合。如 util.inspect (将任意对象转换为字符串)、util.isArray 等等。

  2. fs,文件系统 API,用于操作文件。

    如 fs.readFile (用于异步读取文件)、fs.open、

  3. http,用于创建服务器。如 http.createServer

  4. url,提供一些操作 url 的方法,如获取 url 中的参数

  5. path,用于处理和转换文件路径的工具。

还有等等非常多的模块。

# 全局对象

在浏览器中的 JavaScript,全局对象通常是 window。而在 Node 中,全局对象是 global。

一般来说我们有几个常常使用的变量

  1. process,用于描述当前 Node 进程状态的对象。经常用的有很多,如 process.env.NODE_ENV 用于描述是开发环境(development)或者生产环境(production)
  2. __filename,它表示当前正在执行的文件名。它会输出文件所在位置的绝对路径。
  3. __dirname,它表示当前执行脚本的目录。

# Buffer 正确拼接

不要这样:

var fs = require('fs');
let rs = fs.createReadStream('test.md');
var data = '';
rs.on('data',(chunk) => {
  data+=chunk;// 这里相当于 data = data.toString () + chunk.toString ()
});
rs.on('end',() => {
  console.log(data);
});

这种情况下,宽字节的中文在 Buffer 转 String 时有可能会被截断、无法正常显示(出现乱码)

最好的办法就是将 Buffer 正确拼接成一段大 Buffer 后,再进行转 String 操作。

var chunks = [];
var size = 0;
res.on('data',(chunk) => {
  chunks.push(chunk);
  size += chunk.length;
});
res.on('end', () => {
	var buf = Buffer.concat(chunks,size);
  var str = iconv.decode(buf,'utf8');
  console.log(str);
})

# Node 重点知识

# Node 事件循环

Node 中的事件循环与浏览器中的事件循环不太一样,浏览器环境下的就不详细讲了,见我的其他博客:JavaScript 运行机制笔记 - 前端 | orange's blog = orange's blog = 橙子的博客 (zyczxq.com)

事件循环是 Node 处理非阻塞 I/O 的机制。

Node 中使用 libuv 来进行 I/O 处理,Node 中的 Event Loop 也是基于 libuv 实现的。

Node 中的 Event loop 共分为 6 个阶段,如下:

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
  1. timers(定时器)。执行 setTimeout、setInterval 中到期的 callback。这里其实是由轮询阶段来控制定时器何时执行,而且不是精确的时间,有可能因为操作系统调度而被延迟。

  2. pending callback(待定回调)。执行延迟到下一个循环迭代的 I/O 回调。

  3. idle,prepare。仅内部使用

  4. poll(轮询)。检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。也就是说,如果执行的时间比较长,有可能定时器超时都还没返回 timers 阶段执行定时器。

    如果轮询队列不为空,事件循环将循环访问队列并同步执行直到空。

    如果轮询队列是空的,有两件事:

    • 如果脚本被 setImmediate 调度,则事件循环结束轮询,进入 check(检查)阶段执行那些被调度的脚本
    • 如果没有被 setImmediate 调度,则事件循环会等待回调被添加到队列中,然后立即执行

    在这个过程中,一旦轮询队列为空,事件循环还会检查已经到达时间阈值(或者超时)的计时器,如果有就回到定时器阶段执行对应的回调

  5. check(检测)。执行 setImmediate 回调。

  6. close callbacks(关闭的回调函数)

在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

process.nextTick () 不是事件循环的一部分,它在事件循环的每个阶段完成之后,和微任务一样去执行(但它比其他的微任务优先级都要高

# setImmediate () 对比 setTimeout ()

主要是调用时机不同。

setImmediate 在当前轮询阶段完成后,就执行脚本。而 setTimeout 在最小阈值过后运行脚本。

执行计时器的顺序将根据调用他们的上下文而异。如果两者都从主模块内调用,则受进程性能的约束(顺序是非确定性的)

但是在异步 I/O callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout。因为 I/O 回调在 poll 阶段执行,当执行完后队列为空时,存在 setImmediate 回调的话会先跳转到 check 阶段去执行回调。

# process.nextTick()、setImmediate()

process.nextTick () 比 setImmediate () 触发的更快。如果想设置立即异步执行一个任务,最好不要使用 setTimeout (fn,0),而是使用 process.nextTick () 或 setImmediate ()。

定时器不够准确,很多时候会超时,而且嵌套调用最小单位 4ms、未激活页面最小间隔 1000ms 等都不够精确、底层红黑树的操作时间复杂度为 O(lg (n)),而 nextTick 为 O (1),更高效。

# 与浏览器事件循环的差异?

浏览器会在每个宏任务执行完毕后清空微任务队列

而 Node 中微任务(microtask)在事件循环的各个阶段执行。即每个阶段执行完毕,就会去执行 microtask 队列的任务。

# 内存控制

Node 的内存控制基于 V8 的内存控制,基本是一样的,不再重复,见:https://zyczxq.com/2021/09/23/JavaScript/v8-memoryManage/

# 参考资料

Node.js 中文网 (nodejs.cn)

朴灵的《深入浅出 nodejs》

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

orange 微信支付

微信支付

orange 支付宝

支付宝

orange 贝宝

贝宝