这段时间在学习 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 = { | |
... | |
}; |
它的特点有以下几个:
- CommonJS 以同步的方式加载模块。这一点其实可以理解,因为 Node 通常是运行于服务端,而在服务端模块文件通常存放在本地磁盘,读取速度比起前端通过网络下载要快得多,所以一般没有什么问题(但是因此也不适用于前端)
- CommonJS 输出的是模块的拷贝,这一点与 ESNext 模块不同。模块一旦输出后便独立(即后续更改不影响)
- Node 中对引入过的模块都会进行缓存,减少后续引入的开销。
# 核心模块
util,提供常用函数的集合。如 util.inspect (将任意对象转换为字符串)、util.isArray 等等。
fs,文件系统 API,用于操作文件。
如 fs.readFile (用于异步读取文件)、fs.open、
http,用于创建服务器。如 http.createServer
url,提供一些操作 url 的方法,如获取 url 中的参数
path,用于处理和转换文件路径的工具。
还有等等非常多的模块。
# 全局对象
在浏览器中的 JavaScript,全局对象通常是 window。而在 Node 中,全局对象是 global。
一般来说我们有几个常常使用的变量
- process,用于描述当前 Node 进程状态的对象。经常用的有很多,如 process.env.NODE_ENV 用于描述是开发环境(development)或者生产环境(production)
- __filename,它表示当前正在执行的文件名。它会输出文件所在位置的绝对路径。
- __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 │ | |
└───────────────────────────┘ |
timers(定时器)。执行 setTimeout、setInterval 中到期的 callback。这里其实是由轮询阶段来控制定时器何时执行,而且不是精确的时间,有可能因为操作系统调度而被延迟。
pending callback(待定回调)。执行延迟到下一个循环迭代的 I/O 回调。
idle,prepare。仅内部使用
poll(轮询)。检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()
调度的之外),其余情况 node 将在适当的时候在此阻塞。也就是说,如果执行的时间比较长,有可能定时器超时都还没返回 timers 阶段执行定时器。如果轮询队列不为空,事件循环将循环访问队列并同步执行直到空。
如果轮询队列是空的,有两件事:
- 如果脚本被 setImmediate 调度,则事件循环结束轮询,进入 check(检查)阶段执行那些被调度的脚本
- 如果没有被 setImmediate 调度,则事件循环会等待回调被添加到队列中,然后立即执行
在这个过程中,一旦轮询队列为空,事件循环还会检查已经到达时间阈值(或者超时)的计时器,如果有就回到定时器阶段执行对应的回调。
check(检测)。执行 setImmediate 回调。
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/
# 参考资料
朴灵的《深入浅出 nodejs》