S08-01 Node-基础
[TOC]
概述
Atwood 定律
Atwood 定律:任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。
Stack Overflow 的创立者之一的 Jeff Atwood 在 2007 年提出了著名的 Atwood 定律:
- Any application that can be written in JavaScript, will eventually be written in JavaScript.
- 任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。
但是在发明之初,JavaScript 的目的是应用于在浏览器执行简单的脚本任务,对浏览器以及其中的 DOM 进行各种操作,所以 JavaScript 的应用场景非常受限。
- Atwood 定律更像是一种美好的远景,在当时看来还没有实现的可能性。
- 但是随着 Node 的出现,Atwood 定律已经越来越多的被证实是正确的。
但是为了可以理解 Node.js 到底是如何帮助我们做到这一点的,我们必须了解 JavaScript 是如何被运行的。
浏览器内核
浏览器内核:又名排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样版引擎。
浏览器内核-分类:
- Gecko:早期被 Netscape 和 Mozilla Firefox 浏览器使用;
- Trident:微软开发,被 IE4~IE11 浏览器使用,但是 Edge 浏览器已经转向 Blink;
- Webkit:苹果基于 KHTML 开发、开源的,用于 Safari,Google Chrome 之前也在使用;
- Blink:是 Webkit 的一个分支,Google 开发,目前应用于 Google Chrome、Edge、Opera 等;
浏览器内核-组成:
- WebCore:负责 HTML 解析、布局、渲染等等相关的工作;
- JavaScriptCore:解析、执行 JavaScript 代码;
小程序中编写的 JavaScript 代码就是被 JSCore 执行的

原理图:

但是在这个执行过程中,HTML 解析的时候遇到了 JavaScript 标签,应该怎么办呢?
- 会停止解析 HTML,而去加载和执行 JavaScript 代码;
当然,为什么不直接异步去加载执行 JavaScript 代码,而要在这里停止掉呢?
- 这是因为 JavaScript 代码可以操作我们的 DOM;
- 所以浏览器希望将 HTML 解析的 DOM 和 JavaScript 操作之后的 DOM 放到一起来生成最终的 DOM 树,而不是频繁的去生成新的 DOM 树;
那么,JavaScript 代码由谁来执行呢?
- JavaScript 引擎
JS 引擎
JS 引擎作用:JS 引擎帮助我们将 JS 代码翻译成 CPU 指令来执行。
事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的。但是 CPU 只认识自己的指令集,实际上是机器语言。所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行。
常见 JS 引擎:
- SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者);
- Chakra:微软开发,用于 IE 浏览器;
- JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发;
- V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出;
下面来详细介绍一下 V8 引擎。
V8
V8:是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于Chrome和Node.js等。它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行。V8 可以独立运行,也可以嵌入到任何 C ++应用程序中。
V8 原理图:

V8 引擎本身的源码非常复杂,大概有超过 100w 行 C++代码,但是我们可以简单了解一下它执行 JavaScript 代码的原理:
Parse模块会将 JS 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;
注意:如果函数没有被调用,那么是不会被转换成 AST 的;
Parse 的 V8 官方文档:https://v8.dev/blog/scanner
Ignition是一个解释器,会将 AST 转换成 ByteCode(字节码),同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 注意:如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
- Ignition 的 V8 官方文档:https://v8.dev/blog/ignition-interpreter
TurboFan是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码;
注意 :如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
TurboFan 的 V8 官方文档:https://v8.dev/blog/turbofan-jit
Orinoco 事实上 V8 的内存回收也是其强大的另外一个原因,这里暂时先不展开讨论:
- Orinoco 模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco 的 V8 官方文档:https://v8.dev/blog/trash-talk
Node 基础
概述
Node.js:是一个基于 V8 JavaScript 引擎的 JavaScript 运行时环境。
浏览器、Node 组成:
浏览器组成:
V8 引擎:运行 JS
DOM:解析渲染 HTML、CSS
BOM:浏览器对象模型(浏览器 API)
Node 组成:
V8 引擎
文件系统读/写
网络 IO
加密
压缩解压文件

Node 架构图:

- 我们编写的 JavaScript 代码会经过 V8 引擎,再通过 Node.js 的 Bindings,将任务放到 Libuv 的事件循环中;
- libuv(Unicorn Velociraptor—独角伶盗龙)是使用 C 语言编写的库;
- libuv 提供了事件循环、文件系统读写、网络 IO、线程池等内容;
- 具体内部代码的执行流程,我会在后续专门讲解事件和异步 IO 的原理中详细讲解;
Node 应用场景:
- 包管理工具
- npm
- yarn
- pnpm
- 脚手架
- webpack
- vite
- gulp
- 服务器
- web 服务器
- 代理服务器
- 中间件
- SSR
- 脚本工具
- 工程自动化
- Electron
- vscode
依赖安装
依赖包: Node.js
版本:
- LTS:稳定版
- Current:最新版
安装: 和在 windows 上安装其他软件一样
常用命令:
node --version:查看当前的版本node <path/to/index.js>:运行 index.js 文件
基本使用
Node 的基本使用:
编写 JS 代码:
在
abc.js中编写 JS 代码jsconsole.log('a') console.log('b') console.log('c')终端运行:
通过
node <path>命令执行 JS 文件
版本管理工具
n
介绍: Linux 环境的 Node 版本管理工具
安装: npm i n -g
常用命令:
n --version,查看安装的版本n lts,安装最新的 LTS 版本n latest,安装最新的版本n,查看所有的版本
权限获取:
sodu:Linux 环境中可以通过添加 sudo 前缀,获取执行命令时所需要的权限。
nvm
介绍:Linux 环境的 Node 版本管理工具
nvm-window
介绍:Windows 环境的 Node 版本管理工具
常用命令:
查看 node
nvm list,查看所有安装的版本nvm list installed,显示所有已安装的版本nvm list available,显示所有可以下载的版本
安装 node
nvm install 16.9.0,安装 16.9.0 版本nvm install 16,安装 16 大版本的最新小版本nvm install lts,安装最新的 LTS 版本nvm install latest,安装最新的版本(不是current)
卸载 node
nvm uninstall <版本号>,卸载指定版本的 node
使用指定版本的 node
nvm use <版本号>,使用指定版本的 node
查看 nvm 版本
nvm version,查看 nvm 版本nvm --version,查看 nvm 版本
终端
常用终端:
- CMD
- PowerShell
- Git Bash
VSCode 设置默认终端:
打开 VSCode 终端,按如下顺序点击:

选择默认的终端:

Node 的输入、输出
输入:
1、在终端中运行以下命令:node <index.js> num1=100 num2=200
2、在 js 中通过process.argv获取终端中输入的参数num1、num2

输出:
console.log('hello'),打印消息console.clear(),清空控制台console.trace(),打印执行调用栈
REPL
REPL(Read-Eval-Print Loop,“读取-求值-输出”循环):是一个简单的、交互式的编程环境。
常用命令:
- 开启 REPL:
node + 回车
- 退出 REPL:
Ctrl + C(按2次).exit
- 清空控制台:
- CMD:
cls - Git Bash:
Ctrl + L - Linux:
clear
- CMD:
示例:在 REPL 中的基本操作

全局对象
环境变量
process.env
process.env:当前进程的环境变量。
process.argv
process.argv:一个包含命令行参数的数组。当在命令行中执行 Node.js 脚本时,可以使用该数组来访问传递给脚本的参数。
参数: 接受如下命令行中传递的参数:node <index.js> arg1=xxx arg2=xxx
返回值: 一个包含命令行参数的数组:
- 第一个元素:Node.js 的可执行文件路径
- 第二个元素:被执行的 JavaScript 文件的路径
- 后续元素:命令行传递的参数
示例:process.argv


类似 window
window
window:浏览器环境下的全局对象,Node 环境下没有
注意: 通过 var 定义的变量,会被放入到 window 对象上
global
global:Node 环境下的全局对象,浏览器环境下没有
注意: 通过 var 定义的变量,并不会被放入到 global 对象上
示例:global 对象


globalThis
globalThis:ES2020,是一个跨平台的解决方案。浏览器和 Node 环境下分别指向全局对象 window 和 global
注意:
globalThis === global,Node 环境中二者等价globalThis === window,浏览器环境中二者等价
模块
注意: 以下实际上是模块中的变量
require()
require():(id),用于加载和引用其他 JS 文件或模块。
id:
string,要加载的模块名称或路径。返回:
module:
any,已加载模块的对象。
示例:基本使用
const path = require('path')exports
说明: 包含模块导出内容的空对象。通过向 exports 对象添加属性或方法,可以将它们导出给其他模块使用。
语法:
exports.xxx = value
exports.xxx = function() { ... }
exports.xxx = () => { ... }module
说明: 表示当前模块的对象。每个 JS 文件都是一个独立的模块,可以通过 module 对象来访问和控制模块的行为。
语法:
module.exports = {
xxx: value
}属性:
- module.exports:导出模块内容给其他模块使用。
- module.id:表示当前模块的标识符,通常是文件的绝对路径。
- module.filename:表示当前模块的文件名,通常是文件的绝对路径。
- module.loaded:一个布尔值,表示当前模块是否已经加载完成。
- module.parent:表示当前模块的父级模块。
- module.children:表示当前模块依赖的子模块列表。
方法:
- module.require(id):类似于全局的
require()函数,用于加载和返回指定模块。
__dirname
说明: 当前文件所在目录(绝对路径)
语法:
// /home/user/projects/myapp/app.js
console.log(__dirname) // /home/user/projects/myapp__filename
说明: 当前文件所在目录+文件名称(绝对路径)
语法:
// /home/user/projects/myapp/app.js
console.log(__filename) // /home/user/projects/myapp/app.jsURL
URLSearchParams
说明: 处理 URL 查询字符串的接口
语法:
const params = new URLSearchParams(init?)参数:
- init?:
string|object|URLSearchParams,被解析的目标。string:它会被解析为查询参数,并用于初始化URLSearchParams对象。object:它会被解析为键值对,并用于初始化URLSearchParams对象。URLSearchParams:它会被复制到新的URLSearchParams对象。
返回值:
- params:
URLSearchParams,可以使用URLSearchParams的方法来操作查询参数。
实例方法:
- params.append(name, value):向查询参数中添加一个新的键值对。
- params.delete(name):从查询参数中删除指定名称的键值对。
- params.get(name):获取查询参数中指定名称的第一个值。
- params.getAll(name):获取查询参数中指定名称的所有值的数组。
- params.has(name):检查查询参数中是否存在指定名称的键值对。
- params.set(name, value):将查询参数中指定名称的键值对设置为新的值。
- params.sort():按照名称对查询参数进行排序。
- params.toString():返回表示查询参数的字符串。
示例:解析查询字符串
var baseUrl = 'http://example.com/search?query=tom&age=33'
const url = new URL(baseUrl)
// 获取查询字符串
const queryString = url.search // ?query=tom&age=33
// 方式一:获取 URLSearchParams 对象
const query = url.searchParams // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 方式二:获取 URLSearchParams 对象
const params = new URLSearchParams(query) // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 转化URLSearchParams为对象格式
console.log(Object.fromEntries(params)) // { query: 'tom', age: '33' }
定时器
- window.setTimeout():
(callback,delay?,...args?),用于在指定延迟时间后执行函数或代码的全局方法。 - window.setInterval():
(callback,delay,...args?),用于周期性无限循环调用函数或代码直到被显式取消的全局方法。 - setImmediate():
(callback,...args?),Node,用于安排回调函数在当前事件循环轮次的 "检查" 阶段立即执行的方法。 - window.clearTimeout():
(timeoutID),用于取消由setTimeout()创建的定时器的全局方法。可以阻止尚未执行的定时器回调函数的运行。 - window.clearInterval():
(intervalID),用于终止由setInterval()创建的周期性定时器的全局方法。 - clearImmediate():
(immediateID),Node,用于取消由setImmediate()创建的即将执行的回调函数的方法。
事件循环
定时器
queueMicrotask()
queueMicrotask():(callback),Node,浏览器,用于将回调函数加入到微任务队列的全局函数。微任务会在当前 JS 执行栈清空后、事件循环继续之前执行。
- callback:
()=>void,要在微任务队列中执行的回调函数。该函数不接受任何参数。
示例:
基本用法:
jsqueueMicrotask(() => { console.log('微任务执行'); });使用闭包传递数据:
jslet data = '重要数据'; queueMicrotask(() => { console.log(`处理数据: ${data}`); });使用函数声明:
jsfunction microtaskHandler() { console.log('这是一个微任务'); } queueMicrotask(microtaskHandler);在严格模式下的 this 行为
js'use strict'; queueMicrotask(function() { console.log(this); // 输出: undefined });
核心特性:
微任务执行时机:
微任务在 JS 事件循环中的位置如下:
同步代码执行→nextTick队列→微任务队列→事件循环下一阶段jsconsole.log('脚本开始'); setTimeout(() => { console.log('setTimeout - 宏任务'); }, 0); queueMicrotask(() => { console.log('queueMicrotask - 微任务'); }); process.nextTick(() => { console.log('process.nextTick - 最高优先级'); }); Promise.resolve().then(() => { console.log('Promise.then - 微任务'); }); console.log('脚本结束'); // 输出顺序: // 脚本开始 // 脚本结束 // process.nextTick - 最高优先级 // queueMicrotask - 微任务 // Promise.then - 微任务 // setTimeout - 宏任务对比
process.nextTick():process.nextTick(): Node.js 特有,优先级最高。queueMicrotask(): 浏览器和 Node.js 通用,优先级次之。
对比
Promise.then():queueMicrotask()可以看作是Promise.resolve().then()的语法糖,但更加直观和高效。js// 以下两种方式是等价的: queueMicrotask(() => { console.log('使用 queueMicrotask'); }); Promise.resolve().then(() => { console.log('使用 Promise.resolve().then()'); });
注意事项:
无法取消:
queueMicrotask()没有返回标识符,因此无法取消已经排队的微任务,一旦调用,微任务必定会在当前执行栈结束后执行。
process.nextTick()
process.nextTick():(callback, ...args?),Node,用于将回调函数延迟到当前宏任务的末尾、微任务继续之前执行的函数。
callback:
Function,当前执行栈结束后要执行的函数。...args?:
any,调用回调时要传递的可选参数。
示例:
基本用法:
js// 1. 不带参数 process.nextTick(() => { console.log('在下一个Tick执行'); });js// 2. 带一个参数 process.nextTick((name) => { console.log(`Hello, ${name}!`); }, 'Alice');js// 3. 带多个参数 process.nextTick((a, b, c) => { console.log(`计算结果: ${a + b * c}`); }, 2, 3, 4);js// 4. 使用箭头函数和普通函数 process.nextTick(function() { console.log('这是一个普通函数'); });
核心特性:
执行时机:
process.nextTick()不属于事件循环的任何阶段(定时器、I/O、检查等),它有一个独立的 "nextTick 队列"。在事件循环的每个阶段之间,Node.js 会检查 nextTick 队列,并执行其中的所有回调:
当前JS代码执行→nextTick队列→微任务队列→事件循环下一阶段
jsconsole.log('开始'); setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick'); }); console.log('结束'); // 输出顺序: // 开始 // 结束 // nextTick // setTimeout 或 setImmediate(顺序不确定) // setImmediate 或 setTimeout(顺序不确定)对比
setImmediate():process.nextTick()的优先级 高于setImmediate()。process.nextTick():- 不属于事件循环的任何阶段,它有一个独立的 "nextTick 队列"。
- 会在事件循环当前阶段结束后、进入下一个阶段之前立即执行。
setImmediate():- 会在 事件循环的 "检查" 阶段 执行。
jssetImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick'); }); console.log('当前执行栈'); // 输出顺序(确定性的): // 当前执行栈 // nextTick // setImmediate对比
Promise.then():执行优先级:
process.nextTick()>Promise.then()jsconsole.log('开始'); process.nextTick(() => { console.log('nextTick'); }); Promise.resolve().then(() => { console.log('Promise'); }); console.log('结束'); // 输出顺序: // 开始 // 结束 // nextTick // Promise
注意事项:
无法取消:
process.nextTick()没有返回标识符,因此无法取消已经安排的回调,一旦安排,回调必定会在当前执行栈结束后执行。