Node.js - 快速指南
Node.js - 快速指南
Section titled “Node.js - 快速指南”Node.js - 介绍
Section titled “Node.js - 介绍”什么是 Node.js?
Section titled “什么是 Node.js?”Node.js 是一个开源、跨平台的 JavaScript 运行时环境,构建于 Google Chrome 的 V8 JavaScript 引擎之上。它允许开发者在服务器端执行 JavaScript 代码,从而能够创建后端服务、API 和其他网络应用。你可以在 Node.js 官方网站上找到最新的发布版本和长期支持 (LTS) 版本。
Node.js® 是一个基于 Chrome V8 JavaScript 引擎构建的 JavaScript 运行时。Node.js 使用事件驱动、非阻塞 I/O 模型,使其轻量且高效。Node.js 的包生态系统 npm 是世界上最大的开源库生态系统。
本质上,Node.js 提供了一个运行时环境,允许 JavaScript 在 Web 浏览器之外执行,主要用于服务器端开发。这意味着你可以使用 JavaScript 进行前端和后端开发,从而带来更统一的开发体验。
Node.js 包含一组丰富的内置模块(如 http、fs、path)以及通过其包管理器 npm (Node Package Manager) 提供的庞大第三方包生态系统,这极大地简化了 Web 应用程序开发。
Node.js = V8 引擎 + Libuv + 核心模块 + npm
Node.js 的主要特性
Section titled “Node.js 的主要特性”- 异步和事件驱动: Node.js 中的大多数 API 都是异步的(非阻塞)。Node.js 服务器不会等待 API 调用返回。相反,它会继续执行下一个操作,并使用事件通知系统(事件循环 Event Loop)在之前的调用准备就绪时处理结果。这使得 Node.js 能够高效地处理许多并发连接。
- 速度非常快: 构建于 Chrome 的 V8 JavaScript 引擎之上,Node.js 将 JavaScript 直接编译成原生机器码,从而实现快速执行。
- 单线程但高度可伸缩: Node.js 使用单线程事件循环模型。虽然它在单个主线程中运行代码,但其非阻塞 I/O 操作使其能够高效地处理数千个并发连接,而无需像传统服务器(例如 Apache)那样管理多个线程的开销。对于 CPU 密集型任务,可以使用 Worker Threads。
- 无缓冲(主要是流式处理): Node.js 应用程序有效地使用 Streams 处理数据,通常分块处理数据而无需将大量数据缓冲到内存中,这使其非常适合数据密集型应用程序。
- 跨平台: Node.js 可以在 Windows、macOS 和 Linux 等各种平台上运行。
- 庞大的包生态系统 (npm): npm 是 Node.js 的默认包管理器,提供对数十万个可重用代码包的访问,简化了开发。
- 许可: Node.js 在 MIT 许可下发布。
谁使用 Node.js?
Section titled “谁使用 Node.js?”Node.js 被各种规模的公司广泛采用,从初创企业到大型企业。知名用户包括 Netflix、Uber、PayPal、LinkedIn、eBay、NASA 等等。它用于构建 API、微服务、实时应用程序(如聊天应用和在线游戏)、命令行工具等。
你可以在网上找到使用 Node.js 的公司和项目列表,这些列表通常由社区或 Node.js 基金会维护。
理解以下概念对于有效使用 Node.js 至关重要:Event Loop(处理异步操作)、Modules(CommonJS 和 ES Modules 用于组织代码)、npm(包管理)、Asynchronous Programming(Callbacks, Promises, async/await)、Streams(高效数据处理)和 Buffers(处理二进制数据)。我们将在后续章节中探讨这些内容。
Node.js 用在哪里?
Section titled “Node.js 用在哪里?”Node.js 在以下场景中表现出色:
- I/O 密集型应用: 涉及频繁网络请求或数据库操作的应用(例如,API、Web 服务器)。
- 实时应用: 聊天应用、在线游戏服务、实时协作工具(利用 WebSockets)。
- 数据流应用: 视频和音频流媒体平台。
- 微服务: 构建通过网络通信的小型独立服务。
- 单页应用 (SPAs): 为现代前端框架提供后端 API。
- 工具和命令行接口 (CLIs): 创建构建工具、脚本和开发实用程序。
Node.js 不适合用在哪里?
Section titled “Node.js 不适合用在哪里?”虽然用途广泛,但 Node.js 可能不是处理繁重 CPU 密集型任务(如复杂计算、图像处理)的最佳选择,除非利用 Worker Threads,因为这些任务会阻塞单个主线程。对于此类任务,具有强大多线程能力的语言可能更适合。
Node.js - 环境搭建
Section titled “Node.js - 环境搭建”在线尝试选项
Section titled “在线尝试选项”虽然本指南侧重于本地搭建,但许多在线平台允许你直接在浏览器中尝试 Node.js,无需任何安装。像 Replit、CodeSandbox 或 Node.js 官方网站的 REPL 部分等平台提供了方便的方式来尝试代码片段。
例如,你可以运行这样的简单 JavaScript 代码:
console.log("Hello from Node.js!");使用在线环境非常适合快速测试和学习基本语法。
本地环境搭建
Section titled “本地环境搭建”对于严肃的开发,搭建本地环境至关重要。你需要:
- Node.js 和 npm: 从官方网站 https://nodejs.org/ 下载并安装 Node.js。安装程序包含 npm (Node Package Manager)。强烈建议安装 LTS(长期支持)版本以获得稳定性。
- 版本管理器(可选但推荐): 像 macOS/Linux 上的
nvm(Node Version Manager) 或 Windows 上的nvm-windows等工具允许你轻松安装和切换多个 Node.js 版本。这在处理不同 Node.js 要求的项目时非常有用。 - 文本编辑器/IDE: 一个好的代码编辑器可以提高工作效率。流行的选择包括 Visual Studio Code (VS Code)、WebStorm、Sublime Text 或 Atom。VS Code 是一个免费、强大且广泛使用的选项,具有出色的 JavaScript 和 Node.js 支持。
你将使用文本编辑器编写 Node.js 代码。现代编辑器提供语法高亮、代码补全、调试集成和终端访问等功能,使开发更加顺畅。强烈推荐 VS Code,因为它功能丰富且社区支持良好。
Node.js 源文件通常具有 .js 扩展名(用于 CommonJS 或基本脚本)或 .mjs(用于 ES Modules)。
Node.js 运行时
Section titled “Node.js 运行时”你的搭建核心是 Node.js 运行时本身。它负责解释和执行你的 JavaScript 代码。从 https://nodejs.org/en/download/ 下载适用于你操作系统的安装程序(Windows、macOS、Linux)。
安装步骤(通用):
Section titled “安装步骤(通用):”- Windows: 下载
.msi安装程序并按照安装向导进行。它通常会安装 Node.js 和 npm,并将它们添加到系统的 PATH 环境变量中。 - macOS: 下载
.pkg安装程序并按照说明进行。另外,如果你使用 Homebrew,可以使用brew install node来安装 Node.js。 - Linux: 你可以使用你的发行版包管理器(如 Debian/Ubuntu 的
apt或 Fedora 的yum),或者从 Node.js 网站下载预构建的二进制文件。在 Linux 和 macOS 上,使用nvm通常是更推荐的方法:curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.x/install.sh | bash(请检查 nvm 仓库以获取最新的安装脚本),然后nvm install --lts。
安装完成后,打开你的终端或命令提示符,输入以下命令检查安装的版本:
# 检查 Node.js 版本node -v
# 检查 npm 版本npm -v你应该会看到版本号打印出来,确认安装成功。现在,让我们运行一个简单的文件:
在你的项目目录下创建一个名为 hello.js 的文件:
console.log("Hello, Node.js World!");在终端中使用 node 命令运行该文件:
node hello.js验证输出:
Hello, Node.js World!Node.js - 第一个应用
Section titled “Node.js - 第一个应用”让我们使用 Node.js 内置的 http 模块构建一个非常简单的 Web 服务器。典型的 Node.js 应用程序涉及以下步骤:
- 导入所需的模块: 使用
require关键字(用于 CommonJS 模块,Node.js 的传统方式)或import(用于 ES Modules,现代标准)加载内置或第三方模块。 - 创建服务器: 实例化一个服务器对象,它将监听传入的客户端请求(例如,来自 Web 浏览器)。
- 处理请求并发送响应: 定义服务器处理传入 HTTP 请求并发送回相应 HTTP 响应的逻辑。
步骤 1:导入 http 模块
我们需要内置的 http 模块来创建一个 HTTP 服务器。
// 对于不会重新赋值的变量使用 constconst http = require('http');步骤 2:创建服务器
使用 http.createServer() 方法。此方法接受一个回调函数(请求监听器),该函数针对每个传入请求执行。该函数接收两个参数:request(包含请求详细信息的对象)和 response(用于向客户端发送数据的对象)。
const server = http.createServer((request, response) => { // 设置响应 HTTP 头部,包含 HTTP 状态码和内容类型 response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应体 response.end('Hello World from Node.js Server!\n');});步骤 3:启动服务器并监听端口
告诉服务器监听特定端口上的连接(例如 3000)。我们还会添加一个日志消息,以便知道服务器何时运行。
const port = 3000;const host = '127.0.0.1'; // localhost
server.listen(port, host, () => { console.log(`Server running at http://${host}:${port}/`);});步骤 4:组合并保存代码
将完整的代码保存到一个名为 server.js 的文件中。
// File: server.jsconst http = require('http');
const host = '127.0.0.1'; // localhostconst port = 3000;
const server = http.createServer((request, response) => { // 设置响应 HTTP 头部,包含 HTTP 状态码和内容类型 response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应体 response.end('Hello World from Node.js Server!\n');});
server.listen(port, host, () => { console.log(`Server running at http://${host}:${port}/`);});步骤 5:运行应用
打开你的终端,导航到你保存 server.js 的目录,然后运行:
node server.js在终端中验证输出:
Server running at http://127.0.0.1:3000/向服务器发出请求
Section titled “向服务器发出请求”打开你的 Web 浏览器并导航到 http://127.0.0.1:3000/(或 http://localhost:3000/)。你应该会看到文本:
Hello World from Node.js Server!这确认你的基本 Node.js 服务器正在运行!
Node.js - REPL 终端
Section titled “Node.js - REPL 终端”REPL 代表 Read-Eval-Print Loop(读取-求值-打印-循环)。它是一个交互式编程环境,就像一个 shell 或控制台,你可以在其中输入命令,对其进行求值,查看打印的结果,然后重复此过程。Node.js 自带了一个内置的 REPL。
它执行以下任务:
- Read (读取): 读取用户输入(JavaScript 代码)。
- Eval (求值): 对输入的代码进行求值。
- Print (打印): 打印求值结果。
- Loop (循环): 等待下一次输入。
Node.js REPL 对于快速试验 Node.js 特性、测试小型代码片段以及交互式调试 JavaScript 代码非常有用。
启动 REPL
Section titled “启动 REPL”你只需在终端或命令提示符中输入 node,不带文件名参数即可启动 REPL:
node你会看到 REPL 提示符 (>):
Welcome to Node.js vXX.X.X.Type ".help" for more information.>使用 REPL
Section titled “使用 REPL”你可以输入任何有效的 JavaScript 表达式:
> 1 + 34> 5 * (10 - 2)40> 'Hello' + ' ' + 'REPL''Hello REPL'你可以使用 var、let 或 const 声明变量。如果你在没有 var/let/const 的情况下赋值,它会隐式地成为一个全局变量(在 REPL 上下文中),并且会打印该值。使用 var/let/const 不会立即打印值(它打印 undefined)。
> x = 10 // 隐式全局变量10> let y = 20undefined> const z = 5undefined> x + y - z25> console.log('Value of y:', y)Value of y: 20undefinedREPL 支持多行表达式。当你开始一个代码块(例如使用 {、( 或 [)时,提示符会变为 ...,直到代码块关闭。
> let count = 0undefined> while (count < 3) {... console.log(`Count is: ${count}`);... count++;... }Count is: 0Count is: 1Count is: 2undefined下划线 _ 变量
Section titled “下划线 _ 变量”特殊变量 _ 保存上一个求值表达式的结果。
> 5 * 840> _ + 1050> const lastResult = _undefined> console.log(lastResult)50undefinedREPL 命令
Section titled “REPL 命令”REPL 包含以点号 (.) 开头的特殊命令:
.help:显示此命令列表。.exit:退出 REPL(与Ctrl+D相同)。.break:中止多行输入。有时在你卡住时很有用。.clear:将 REPL 上下文重置为空对象,并清除任何多行表达式。.save <filename>:将当前 REPL 会话历史记录保存到文件。.load <filename>:从文件加载代码到当前 REPL 会话。.editor:进入编辑器模式,以便更容易进行多行编辑。
Ctrl + C:终止当前命令(按一次)或退出 REPL(按两次)。Ctrl + D:退出 REPL。上/下箭头键:在命令历史记录中导航。Tab 键:自动补全变量、关键字和对象属性。
使用 .save 和 .load 的示例
Section titled “使用 .save 和 .load 的示例”> let message = 'Saving this';undefined> console.log(message);Saving thisundefined> .save my_session.jsSession saved to: my_session.js> .clearClearing context...> console.log(message);Uncaught ReferenceError: message is not defined> .load my_session.jslet message = 'Saving this';console.log(message);
Saving thisundefined> message'Saving this'Node.js - npm (Node 包管理器)
Section titled “Node.js - npm (Node 包管理器)”npm 代表 Node Package Manager(Node 包管理器)。它是世界上最大的软件注册表,也是 Node.js 的默认包管理器。当你安装 Node.js 时,npm 会自动包含在内。
npm 主要有两个用途:
- 在线注册表: 一个托管着数十万个免费、可重用 Node.js 包(也称为模块或库)的在线仓库 (https://www.npmjs.com/)。
- 命令行工具 (CLI): 一个在终端中用于安装、更新、管理和发布项目 Node.js 包的工具。
你可以通过输入以下命令验证 npm 安装:
npm --version此命令将显示已安装的 npm 版本。
package.json 和 node_modules
Section titled “package.json 和 node_modules”每个 Node.js 项目在其根目录下都应该有一个 package.json 文件。此文件包含有关项目的元数据,包括:
- 项目名称、版本、描述、作者、许可证。
- 用于自动化任务的脚本(如启动应用、运行测试)。
- Dependencies (依赖): 应用程序运行所需的包。
- DevDependencies (开发依赖): 仅在开发过程中需要的包(如测试框架、构建工具)。
你可以在项目目录中运行 npm init 并按照提示创建 package.json 文件,或使用 npm init -y 接受默认设置。
当你使用 npm 安装包时,它们会被下载并放置在项目目录内的 node_modules 文件夹中。package.json 文件列出了这些依赖项,并且会自动生成一个 package-lock.json 文件,以锁定每个安装的依赖项的具体版本,确保在不同环境中安装的一致性。
本地安装 vs. 全局安装
Section titled “本地安装 vs. 全局安装”本地安装(默认): 本地安装的包放置在当前项目的 node_modules 文件夹中,并列在 package.json 中。这是管理项目特定依赖项的标准方式。你在代码中使用 require() 或 import 来使用它们。
# 本地安装 'lodash' 并将其添加到 package.json 中的 dependenciesnpm install lodash
# 或者显式地保存为依赖项(较旧的 npm 版本需要 --save)npm install lodash --save
# 安装一个开发依赖项(例如,一个测试工具)npm install jest --save-dev# 或者使用简写方式npm install -D jest全局安装: 全局安装的包通常是命令行工具,你想在系统的任何地方使用它们。它们存储在系统范围内的位置(位置因操作系统而异)。使用 -g 标志。
# 全局安装 'nodemon'(一个在文件更改时重启应用的工具)npm install -g nodemon通常,项目依赖项优先选择本地安装,只有 CLI 工具才选择全局安装。
常用 npm 命令
Section titled “常用 npm 命令”npm init [-y]:初始化一个新项目,创建package.json文件。npm install <package_name>:本地安装一个包并将其添加到dependencies中。npm install <package_name> -D或npm install <package_name> --save-dev:本地安装一个包并将其添加到devDependencies中。npm install <package_name> -g:全局安装一个包。npm install:安装package.json中列出的所有依赖项(根据package-lock.json读取特定版本)。在克隆项目后使用此命令。npm ci:代表 ‘clean install’(干净安装)。它会删除node_modules并严格按照package-lock.json中指定的版本安装依赖项。对于自动化构建/CI 环境更快更可靠。npm update [<package_name>]:将包更新到package.json中版本范围允许的最新版本,并更新package-lock.json。npm uninstall <package_name>:本地移除一个包并从package.json中删除。npm uninstall <package_name> -g:移除一个全局安装的包。npm list:列出已安装的本地包。npm list -g --depth=0:列出全局安装的包(仅顶层)。npm search <keyword>:在 npm 注册表中搜索包。npm run <script_name>:执行package.json中scripts部分定义的脚本。npx <command>:执行一个包提供的命令,而无需全局或本地安装该包。对于按需运行 CLI 工具很有用(例如,npx create-react-app my-app)。
示例:安装和使用一个包
Section titled “示例:安装和使用一个包”-
在终端中导航到你的项目目录。
-
初始化项目(如果尚未初始化):
npm init -y- 安装一个包,例如
lodash(一个流行的实用工具库):
npm install lodash这将创建 node_modules(如果不存在),下载 lodash 及其依赖项到其中,并将 lodash 添加到 package.json 的 dependencies 部分。
- 在你的 Node.js 文件中使用该包(例如
app.js):
const _ = require('lodash');
const numbers = [1, 5, 8, 10, 1, 5];const uniqueNumbers = _.uniq(numbers);
console.log('Original numbers:', numbers);console.log('Unique numbers:', uniqueNumbers);- 运行你的应用:
node app.js输出:Original numbers: [ 1, 5, 8, 10, 1, 5 ]Unique numbers: [ 1, 5, 8, 10 ]理解 package.json
Section titled “理解 package.json”这是一个 package.json 文件的极简示例:
{ "name": "my-node-app", "version": "1.0.0", "description": "A simple Node.js application", "main": "server.js", // 应用程序的主要入口文件 "scripts": { "start": "node server.js", // 使用 'npm start' 运行 "test": "echo \"Error: no test specified\" && exit 1" // 使用 'npm test' 运行 }, "keywords": [ "node", "example", "server" ], "author": "Your Name <your.email@example.com>", "license": "ISC", // 或 'MIT' 等 "dependencies": { "lodash": "^4.17.21" // 示例依赖,'^' 允许小版本和补丁更新 }, "devDependencies": { "nodemon": "^2.0.15" // 示例开发依赖 }}name、version、main、scripts、dependencies 和 devDependencies 等关键字段对于有效管理你的项目及其依赖关系至关重要。
Node.js - 异步编程:Callbacks, Promises, Async/Await
Section titled “Node.js - 异步编程:Callbacks, Promises, Async/Await”Node.js 最显著的特征之一是其异步、非阻塞的特性。这意味着像读取文件、发起网络请求或查询数据库等操作不会阻塞程序的执行。相反,Node.js 使用各种机制在后台处理这些操作,并在它们完成时通知你的代码。
从历史上看,Callbacks(回调函数)是处理异步操作的主要方式。虽然它是基础,但它们可能导致复杂的嵌套结构,称为“Callback Hell”(回调地狱)。现代 JavaScript 引入了 Promises 和 async/await 语法,它们提供了更清晰、更易于管理的方式来处理异步代码。
1. Callbacks(回调函数)
Section titled “1. Callbacks(回调函数)”Callback 简单来说就是一个函数作为参数传递给另一个函数,然后在异步操作完成时被调用。在 Node.js 中,Callback 通常遵循错误优先约定(error-first convention):Callback 函数的第一个参数保留用于错误对象(如果没有错误则为 null),后续参数包含结果数据。
阻塞(同步)代码示例:
Section titled “阻塞(同步)代码示例:”同步函数会阻塞执行,直到它们完成。readFileSync 就是一个例子。
// 创建一个名为 input.txt 的文件,包含一些文本。// 例如:“Node.js is asynchronous!”
const fs = require('fs');
try { // 这会阻塞执行直到文件被读取 const data = fs.readFileSync('input.txt', 'utf8'); console.log("File content (sync):", data);} catch (err) { console.error("Error reading file sync:", err);}
console.log("Program Ended (after sync read)");输出(假设 input.txt 存在):File content (sync): Node.js is asynchronous!Program Ended (after sync read)非阻塞(异步)代码示例(使用 Callback):
Section titled “非阻塞(异步)代码示例(使用 Callback):”异步函数如 readFile 不会阻塞。它们接受一个 Callback 函数,在操作完成后执行。
const fs = require('fs');
// 这行代码开始读取文件,但不会阻塞fs.readFile('input.txt', 'utf8', (err, data) => { // 这个 Callback 函数会在稍后执行,当读取完成或发生错误时 if (err) { console.error("Error reading file async:", err); return; } console.log("File content (async):", data);});
// 这行代码几乎立即执行,可能在文件读取完成之前console.log("Program Ended (likely before async read completes)");典型输出:Program Ended (likely before async read completes)File content (async): Node.js is asynchronous!注意 “Program Ended” 是如何先于文件内容输出的,这展示了非阻塞特性。
2. Promises
Section titled “2. Promises”Promises 提供了一种更结构化的方式来处理异步操作。Promise 代表一个异步操作的最终结果(或失败)。它可以处于三种状态之一:pending(待定)、fulfilled(已完成/ resolved)或 rejected(已拒绝)。
Promises 使用 .then() 来处理成功的结果,使用 .catch() 来处理错误。许多现代 Node.js API(特别是 fs.promises 等模块)返回 Promises。
使用 fs.promises 的示例:
Section titled “使用 fs.promises 的示例:”const fs = require('fs').promises; // 导入基于 Promise 的版本
fs.readFile('input.txt', 'utf8') .then(data => { // 如果 Promise 完成(读取成功)则执行 console.log("File content (Promise):", data); }) .catch(err => { // 如果 Promise 被拒绝(发生错误)则执行 console.error("Error reading file (Promise):", err); }) .finally(() => { // 无论成功还是失败都会执行(可选) console.log("File read attempt finished (Promise)."); });
console.log("Program continues while promise is pending...");典型输出:Program continues while promise is pending...File content (Promise): Node.js is asynchronous!File read attempt finished (Promise).3. Async/Await
Section titled “3. Async/Await”async 和 await 是基于 Promises 的语法糖,它们使异步代码看起来和行为更像同步代码,提高了可读性。
async关键字用于声明一个处理异步操作的函数。await关键字只能在async函数内部使用。它会暂停async函数的执行,直到它等待的 Promise 被解决(resolved)或拒绝(rejected)。
使用 async/await 和 fs.promises 的示例:
Section titled “使用 async/await 和 fs.promises 的示例:”const fs = require('fs').promises;
// 声明一个 async 函数async function readFileAsync() { console.log("Attempting to read file..."); try { // await 在这里暂停执行,直到 Promise 解决 const data = await fs.readFile('input.txt', 'utf8'); console.log("File content (async/await):", data); } catch (err) { // 可以使用 try...catch 捕获 await 的 Promise 中发生的错误 console.error("Error reading file (async/await):", err); } console.log("Async function finished.");}
// 调用 async 函数readFileAsync();
console.log("Program continues after calling async function...");典型输出:Attempting to read file...Program continues after calling async function...File content (async/await): Node.js is asynchronous!Async function finished.结论: 虽然 callbacks 是理解 Node.js 历史的基础,但 Promises 特别是 async/await 是编写更清晰、更易读、更易维护的异步代码的现代标准。
Node.js - 事件循环 (Event Loop)
Section titled “Node.js - 事件循环 (Event Loop)”尽管 Node.js 主要采用单线程模型,但 Event Loop 是其非阻塞 I/O 和并发能力背后的秘密武器。它是一种机制,允许 Node.js 在可能的情况下,将非阻塞操作(如读取文件、网络请求)卸载到系统内核进行。
工作原理(简化版):
Section titled “工作原理(简化版):”- 代码执行: 你的 JavaScript 代码在单个主线程中运行。
- 异步操作: 当调用异步操作(例如
fs.readFile)时,Node.js 不会等待。它会启动该操作(通常由底层的 C++ 代码和像 libuv 这样的库处理,它们可能使用系统线程)并注册一个 Callback 函数,以便稍后执行。 - 事件队列: 当异步操作完成时(例如,文件读取完成、网络响应到达),其相应的 Callback 函数被放入一个事件队列中(具体来说,不同类型的事件如 I/O、定时器等有不同的队列)。
- 事件循环: Event Loop 会持续检查主执行堆栈(运行同步代码的地方)是否为空。如果为空,Event Loop 会从相应的事件队列中取出下一个 Callback 函数,并将其推送到堆栈上执行。
- Callback 执行: Callback 函数运行,可能会执行更多的操作,其中一些本身也可能是异步的。
- 重复: 循环继续,只要有待处理的事件或者进程尚未退出,就会一直处理事件并执行 Callback 函数。
可视化比喻:餐厅服务员
想象一下在一家繁忙的餐厅(你的应用程序)里有一位服务员(Node.js 线程)。
- 服务员点餐(发起一个异步操作,如数据库查询)。
- 服务员不会在厨房里等待食物(阻塞),而是立即去接待下一位顾客点餐(执行下一行代码)。
- 当厨房(系统内核/libuv)准备好食物(完成异步操作)时,会按铃(将 Callback 函数放入事件队列)。
- 服务员(Event Loop)空闲时(调用堆栈为空),听到铃声,取走食物(执行 Callback 函数),然后端给相应的桌子。
这使得一个服务员能够并发地服务许多桌子,而不会因为等待任何一个订单而被卡住。
- Event Loop 使 Node.js 能够以单个主线程处理高并发。
- I/O 操作是非阻塞的;Callbacks/Promises/async 函数处理它们的完成。
- CPU 密集型的同步代码 会 阻塞 Event Loop,可能降低性能。对于此类任务,应使用 Worker Threads。
- 理解 Event Loop 有助于编写高效且响应迅速的 Node.js 应用程序。
如需深入了解,请查阅 Node.js 官方关于 Event Loop、Timers 和 process.nextTick() 的文档。
Node.js - 事件触发器 (Event Emitter)
Section titled “Node.js - 事件触发器 (Event Emitter)”Node.js 的大部分核心异步功能都围绕着事件驱动架构构建。称为“触发器”(emitters)的对象会触发命名事件,这些事件会引起先前注册的“监听器”(listener)函数被调用。
events 模块中的 EventEmitter 类是这种模式的核心。许多内置的 Node.js 对象(如 HTTP 服务器、流 streams)都继承自 EventEmitter。
导入和使用 EventEmitter:
Section titled “导入和使用 EventEmitter:”const EventEmitter = require('events');
// 创建一个自定义触发器实例class MyEmitter extends EventEmitter {}const myEmitter = new MyEmitter();- 触发事件: 使用
emitter.emit(eventName[, ...args])来触发一个事件。 - 监听事件: 使用
emitter.on(eventName, listener)或emitter.addListener(eventName, listener)注册一个函数 (listener),该函数将在eventName被触发时被调用。 - 只监听一次: 使用
emitter.once(eventName, listener)注册一个监听器,该监听器只在事件下次触发时被调用一次,之后会自动移除。 - 移除监听器: 使用
emitter.removeListener(eventName, listener)或emitter.off(eventName, listener)来停止监听。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 监听器函数 1const listener1 = (arg1, arg2) => { console.log('Event occurred with args:', arg1, arg2);};
// 监听器函数 2 (只会运行一次)const listener2 = () => { console.log('This listener runs only once.');};
// 注册监听器myEmitter.on('someEvent', listener1);myEmitter.once('someEvent', listener2);myEmitter.on('someEvent', () => { console.log('Another listener for someEvent.');});
console.log('Emitting event for the first time:');myEmitter.emit('someEvent', 'Hello', 'World'); // 所有监听器运行
console.log('\nEmitting event for the second time:');myEmitter.emit('someEvent', 'Second', 'Time'); // listener2 已经移除
// 移除 listener1myEmitter.removeListener('someEvent', listener1);console.log('\nEmitting event after removing listener1:');myEmitter.emit('someEvent', 'Third', 'Time'); // 只有匿名监听器运行
// 检查监听器数量console.log(`\nListeners for someEvent: ${myEmitter.listenerCount('someEvent')}`);输出:Emitting event for the first time:Event occurred with args: Hello WorldThis listener runs only once.Another listener for someEvent.
Emitting event for the second time:Event occurred with args: Second TimeAnother listener for someEvent.
Emitting event after removing listener1:Another listener for someEvent.
Listeners for someEvent: 1'error': 一个特殊事件。如果一个EventEmitter触发了'error'事件,并且没有为它注册监听器,Node.js 将抛出错误,打印堆栈跟踪,并退出进程。务必注册错误处理程序!'newListener': 当添加监听器时触发。'removeListener': 当移除监听器时触发。
EventEmitter 模式对于在 Node.js 中创建模块化和解耦的组件至关重要,尤其是在处理流、网络操作和其他异步活动时。
Node.js - 缓冲器 (Buffers)
Section titled “Node.js - 缓冲器 (Buffers)”JavaScript 传统上很好地处理 Unicode 字符串,但缺乏处理原始二进制数据的健壮机制。在处理网络流、文件系统操作或 TCP/IP 等协议时,需要直接处理原始字节序列(八位字节流)。Node.js 引入了 Buffer 类来直接处理二进制数据。
Buffer 对象类似于整数数组(表示 0-255 范围内的字节),但它们对应于在 V8 JavaScript 引擎堆外分配的固定大小的原始内存块。Buffer 的大小一旦创建就不能改变。
Buffer 类是全局可用的;你不需要 require 它。
创建 Buffers(现代方法)
Section titled “创建 Buffers(现代方法)”旧的 new Buffer() 构造函数因安全漏洞已被弃用。应改用以下静态方法:
Buffer.alloc(size[, fill[, encoding]]): 创建一个指定size(字节)的新的、已初始化(零填充) 的Buffer。出于安全考虑,这是创建已知大小的新 buffer 的首选方法。Buffer.allocUnsafe(size): 创建一个指定size的新的、未初始化 的Buffer。比Buffer.alloc()快,但分配的内存段可能包含旧的、潜在的敏感数据。仅在你确定会立即完全覆盖整个 buffer 或性能至关重要时使用。Buffer.from(data[, encoding]): 创建一个包含提供的data副本 的新Buffer。data可以是一个字符串、数组、Buffer、ArrayBuffer 或 TypedArray。
// 创建一个大小为 10 字节的零填充 Bufferconst buf1 = Buffer.alloc(10);console.log('buf1 (alloc):', buf1);
// 创建一个大小为 10 字节的未初始化 Buffer (内容是不可预测的)// 读取前必须填充!const buf2 = Buffer.allocUnsafe(10);console.log('buf2 (allocUnsafe):', buf2);// 立即填充 buf2buf2.fill(0);console.log('buf2 (filled):', buf2);
// 从字符串创建 Buffer (UTF-8 是默认编码)const buf3 = Buffer.from('Hello Buffer');console.log('buf3 (from string):', buf3);console.log('buf3 decoded:', buf3.toString());
// 从字节数组 (0-255) 创建 Bufferconst buf4 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 'Hello' 的 ASCII 码console.log('buf4 (from array):', buf4);console.log('buf4 decoded:', buf4.toString('ascii'));
// 从另一个 Buffer 创建 Buffer (创建副本)const buf5 = Buffer.from(buf3);console.log('buf5 (from buffer):', buf5);写入 Buffers
Section titled “写入 Buffers”主要方法是 buf.write():
语法:`buf.write(string[, offset][, length][, encoding])`string:要写入的字符串。offset:开始写入的索引(默认:0)。length:要写入的最大字节数(默认:buf.length - offset)。encoding:字符串编码(默认:‘utf8’)。
返回值
返回写入的字节数。如果 buffer 没有足够的空间容纳整个字符串(考虑到 offset 和 length),则只会写入部分字符串。
const buf = Buffer.alloc(20);const len = buf.write('Node.js Buffers');
console.log(`Bytes written: ${len}`); // 输出:Bytes written: 15console.log(buf.toString()); // 输出:Node.js Buffers// Buffer 的其余部分保持零填充从 Buffers 读取
Section titled “从 Buffers 读取”使用 buf.toString() 将 Buffer 数据解码为字符串:
语法:`buf.toString([encoding][, start][, end])`encoding:使用的编码(默认:‘utf8’)。常用编码:‘utf8’、‘ascii’、‘base64’、‘hex’。start:开始读取的索引(默认:0)。end:停止读取的索引(不包含,默认:buf.length)。
返回 buffer 数据(或其一部分)的解码字符串表示。
const buf = Buffer.alloc(26);for (let i = 0; i < 26; i++) { buf[i] = i + 97; // 填充 ASCII 'a' 到 'z'}
console.log(buf.toString('ascii')); // 输出:abcdefghijklmnopqrstuvwxyzconsole.log(buf.toString('ascii', 0, 5)); // 输出:abcdeconsole.log(buf.toString('utf8', 5, 10)); // 输出:fghij (这里 UTF8 和 ASCII 兼容)console.log(buf.toString(undefined, 10, 15)); // 编码默认为 'utf8',输出:klmno将 Buffer 转换为 JSON
Section titled “将 Buffer 转换为 JSON”buf.toJSON() 方法提供适合序列化的 JSON 表示。
buf.toJSON()返回一个对象 { type: 'Buffer', data: [byte_values_array] }。请注意,JSON.stringify() 在 stringify Buffer 实例时会隐式调用此方法。
const buf = Buffer.from('Hello');const json = buf.toJSON();
console.log(json);// 输出:{ type: 'Buffer', data: [ 72, 101, 108, 108, 111 ] }
const jsonString = JSON.stringify(buf);console.log(jsonString);// 输出:"{\"type\":\"Buffer\",\"data\":[72,101,108,108,111]}"连接 Buffers
Section titled “连接 Buffers”使用静态方法 Buffer.concat() 将多个 Buffers 合并为一个新的 Buffer。
Buffer.concat(list[, totalLength])list:要连接的 Buffer 实例数组。totalLength(可选):合并后 buffer 的总长度。提供此参数可以提高性能,因为它避免了额外的循环来计算长度。
返回一个包含 list 中 buffers 连接数据的新 Buffer。
const buffer1 = Buffer.from('Node.js ');const buffer2 = Buffer.from('is awesome!');const buffer3 = Buffer.concat([buffer1, buffer2]);
console.log("buffer3 content: " + buffer3.toString());// 输出:buffer3 content: Node.js is awesome!比较 Buffers
Section titled “比较 Buffers”使用 buf.compare(otherBuffer) 按字典顺序比较两个 buffers 的内容。
buf.compare(otherBuffer);otherBuffer:与buf进行比较的另一个 Buffer 实例。
返回:如果 buf 等于 otherBuffer,则返回 0;如果在排序时 buf 应该排在 otherBuffer 之后,则返回 1;如果在排序时 buf 应该排在 otherBuffer 之前,则返回 -1。
const buffer1 = Buffer.from('ABC');const buffer2 = Buffer.from('ABCD');const buffer3 = Buffer.from('ABC');
console.log(buffer1.compare(buffer2)); // 输出:-1 (buffer1 排在 buffer2 之前)console.log(buffer2.compare(buffer1)); // 输出:1 (buffer2 排在 buffer1 之后)console.log(buffer1.compare(buffer3)); // 输出:0 (buffer1 与 buffer3 相同)
// 可用于对 Buffer 数组进行排序const arr = [buffer2, buffer1, buffer3];arr.sort(Buffer.compare);console.log(arr.map(b => b.toString())); // 输出:[ 'ABC', 'ABC', 'ABCD' ]复制 Buffer
Section titled “复制 Buffer”使用 buf.copy() 将数据从一个 buffer (buf) 复制到另一个 buffer (targetBuffer)。
buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd])targetBuffer:目标 Buffer 对象。targetStart:在targetBuffer中开始写入的索引(默认:0)。sourceStart:在源buf中开始复制的索引(默认:0)。sourceEnd:在源buf中停止复制的索引(不包含,默认:buf.length)。
返回复制的字节数。
const buffer1 = Buffer.from('Source Data');const buffer2 = Buffer.alloc(15); // 目标 buffer
// 将 buffer1 的全部内容复制到 buffer2buffer1.copy(buffer2);console.log("buffer2 content: " + buffer2.toString()); // 输出:buffer2 content: Source Data
// 将 buffer1 的一部分 ('Data') 复制到 buffer2,从索引 3 开始const buffer3 = Buffer.alloc(10).fill(' '); // 用空格填充buffer1.copy(buffer3, 3, 7, 11); // targetStart=3, sourceStart=7, sourceEnd=11 ('Data')console.log("buffer3 content: '" + buffer3.toString() + "'"); // 输出:buffer3 content: ' Data '切片 Buffer
Section titled “切片 Buffer”使用 buf.slice() 创建一个新的 Buffer,它引用原始 buffer 内存的一部分。这是一种浅拷贝——对切片的修改会影响原始 Buffer,反之亦然。
buf.slice([start][, end])start:切片开始的索引(包含,默认:0)。end:切片结束的索引(不包含,默认:buffer.length)。
返回一个表示原始 buffer 内存指定部分的新 Buffer 实例。
const buffer1 = Buffer.from('Node.js Buffers');
// 创建一个包含 'Buffers' 的切片const buffer2 = buffer1.slice(8, 15);console.log("buffer2 content: " + buffer2.toString()); // 输出:buffer2 content: Buffers
// 修改切片buffer2[0] = 0x58; // 将 'B' (0x42) 改为 'X' (0x58)
console.log("Modified slice: " + buffer2.toString()); // 输出:Modified slice: Xuffers// 原始 buffer 也被修改了!console.log("Original buffer after slice mod: " + buffer1.toString()); // 输出:Original buffer after slice mod: Node.js XuffersBuffer 长度
Section titled “Buffer 长度”访问 buf.length 属性获取 buffer 分配的大小(字节)。
buf.length;返回为 Buffer 实例分配的字节数。
const buffer = Buffer.from('Example');// length 是字节数,不一定是字符数console.log("buffer length: " + buffer.length); // 输出:buffer length: 7
const bufAlloc = Buffer.alloc(1024);console.log("bufAlloc length: " + bufAlloc.length); // 输出:bufAlloc length: 1024Buffer 方法参考
Section titled “Buffer 方法参考”Buffer 类还提供了许多其他方法用于读取和写入特定数字类型(整数、浮点数、双精度数)以及不同字节顺序(endianness)、填充 buffers、检查编码等。查阅 Node.js 官方 Buffer 文档以获取完整的列表和详细解释。