现在位置: 首页 > Node.js 教程 > 正文

Node.js 基础概念

Node.js 是一个基于 Chrome V8 JavaScript 引擎构建的 JavaScript 运行时环境。简单来说,Node.js 让 JavaScript 可以在服务器端运行,而不仅仅局限于浏览器中。

Node.js 的核心特点

1. 单线程事件循环

  • Node.js 使用单线程的事件循环模型
  • 通过事件驱动和回调函数处理并发请求
  • 避免了传统多线程编程中的线程切换开销

2. 非阻塞 I/O

  • 所有 I/O 操作(文件读写、网络请求等)都是异步的
  • 当等待 I/O 操作完成时,程序不会被阻塞,可以继续处理其他任务
  • 大大提高了应用程序的吞吐量

3. 跨平台

  • 支持 Windows、macOS、Linux 等多种操作系统
  • 一次编写,到处运行

4. 丰富的生态系统

  • npm(Node Package Manager)拥有数百万个开源包
  • 活跃的开发者社区

与传统服务器端技术的区别

1、传统服务器端技术(如 Apache + PHP):

请求1 → 创建线程1 → 处理请求 → 返回响应 → 销毁线程1
请求2 → 创建线程2 → 处理请求 → 返回响应 → 销毁线程2
请求3 → 创建线程3 → 处理请求 → 返回响应 → 销毁线程3

2、Node.js 的处理方式:

请求1 → 事件循环 → 处理请求 → 返回响应
请求2 → 事件循环 → 处理请求 → 返回响应  (复用同一线程)
请求3 → 事件循环 → 处理请求 → 返回响应

特性 传统多线程模型 Node.js 单线程模型
内存占用 每个线程占用 2MB 左右 单线程,内存占用少
并发处理 线程数量限制并发数 事件循环处理高并发
上下文切换 频繁的线程切换开销 无线程切换开销
编程复杂度 需要处理线程同步 避免了锁和线程同步问题
适用场景 CPU 密集型任务 I/O 密集型任务

Node.js 的应用场景

1、适合的应用场景

Web 应用程序

  • RESTful API 服务
  • 单页应用(SPA)的后端服务
  • 实时 Web 应用

实时应用

  • 聊天应用
  • 在线游戏
  • 协作工具(如在线文档编辑)

微服务架构

  • 轻量级的微服务
  • API 网关
  • 服务间通信

工具和命令行应用

  • 构建工具(如 Webpack、Gulp)
  • 脚手架工具
  • 自动化脚本

物联网(IoT)应用

  • 设备数据收集
  • 传感器数据处理

2、不适合的应用场景

CPU 密集型任务

  • 图像/视频处理
  • 复杂的数学计算
  • 大数据分析

需要大量计算的应用

  • 机器学习训练
  • 科学计算
  • 加密货币挖矿

事件驱动和非阻塞 I/O 模型

传统阻塞 I/O 模型示例:

实例

// 伪代码 - 阻塞式操作
// 每次 readFileSync 都会"卡住"程序,等待文件读取完成后才继续往下执行
const data1 = readFileSync('file1.txt');  // 程序在此暂停,等待读取完成
const data2 = readFileSync('file2.txt');  // data1 读完后,再暂停等待
const data3 = readFileSync('file3.txt');  // data2 读完后,再暂停等待
console.log('所有文件读取完成');          // 三个文件全部读完后才执行到这里
// 总耗时 = 读取file1的时间 + 读取file2的时间 + 读取file3的时间

Node.js 非阻塞 I/O 模型示例:

实例

// Node.js 异步操作
const fs = require('fs');

// 三个读文件的操作几乎同时发出,程序不会等待任何一个完成
fs.readFile('file1.txt', (err, data1) => {
    // 这个回调函数会在 file1.txt 读取完成后自动被调用
    console.log('文件1读取完成');
});

fs.readFile('file2.txt', (err, data2) => {
    // 这个回调函数会在 file2.txt 读取完成后自动被调用
    console.log('文件2读取完成');
});

fs.readFile('file3.txt', (err, data3) => {
    // 这个回调函数会在 file3.txt 读取完成后自动被调用
    console.log('文件3读取完成');
});

// 注意:这行代码会最先执行!因为上面三个读文件操作是异步的,
// 发出读取请求后程序立刻继续往下走,不会等待文件读取完成
console.log('程序继续执行,不等待文件读取');

// 实际输出顺序示例:
// 程序继续执行,不等待文件读取   ← 最先打印
// 文件2读取完成                  ← 哪个文件先读完就先打印,顺序不固定
// 文件1读取完成
// 文件3读取完成
// 总耗时 ≈ 三个文件中读取最慢的那一个的时间(并发执行,而非顺序叠加)

事件循环机制:

事件循环是 Node.js 实现非阻塞 I/O 的核心机制。它持续运行,不断检查是否有待执行的任务,从而实现"单线程处理并发"的效果:

  1. 调用栈(Call Stack):执行同步代码,函数调用时入栈,执行完毕后出栈
  2. 事件队列(Event Queue):存储异步操作完成后的回调函数,等待调用栈空闲时依次执行
  3. 事件循环(Event Loop):持续监控调用栈和事件队列,一旦调用栈为空,就将事件队列中的回调取出放入调用栈执行

事件循环按照固定顺序依次处理以下几个阶段:

┌───────────────────────────┐
┌─>│           timers          │  ← 执行 setTimeout、setInterval 的到期回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← 执行上一轮循环中延迟的 I/O 回调(如某些系统错误回调)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← Node.js 内部使用,开发者一般不需要关心
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  ← 核心阶段:获取新的 I/O 事件并执行其回调(如文件读取、网络请求完成)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  ← 执行 setImmediate 的回调(在 poll 阶段之后立即执行)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  ← 执行关闭事件的回调,如 socket.on('close', ...)
   └───────────────────────────┘

对于初学者来说,最需要记住的是:同步代码总是优先执行,异步回调(如文件读取完成的处理)会在调用栈清空后才被执行。


JavaScript 运行时环境

V8 引擎简介

V8 是 Google 开发的高性能 JavaScript 引擎,也是 Chrome 浏览器的核心组件。

Node.js 使用 V8 引擎来执行 JavaScript 代码。

V8 引擎的特点:

即时编译(JIT)

  • 将 JavaScript 代码直接编译为机器码
  • 无需中间字节码,执行效率更高

垃圾收集

  • 自动内存管理,开发者无需手动释放内存
  • 使用分代垃圾收集算法,将内存分为新生代和老生代分别管理,提升回收效率

优化技术

  • 内联缓存(Inline Caching):缓存对象属性的查找结果,避免重复查找
  • 隐藏类(Hidden Classes):将动态语言的对象结构静态化,加速属性访问
  • 动态优化:运行时分析热点代码并进行针对性优化

浏览器 JavaScript vs Node.js JavaScript

虽然都使用 JavaScript 语言,但运行环境的差异导致了一些重要区别:

相同点:

  • 都使用相同的 JavaScript 语法
  • 都支持 ES6+ 特性
  • 都使用 V8 引擎(Chrome 浏览器)

不同点:

特性 浏览器 JavaScript Node.js JavaScript
全局对象 window global
模块系统 ES6 modules, AMD CommonJS, ES6 modules
文件系统访问 不可访问(出于安全限制) 完全访问
网络请求 XMLHttpRequest, Fetch http, https 模块
DOM 操作 支持(操作页面元素) 不支持(服务器端无页面)
进程控制 不支持 支持(可读取环境变量、退出进程等)

浏览器环境示例:

实例

// 浏览器中的全局对象和 DOM 操作
console.log(window);                      // 浏览器全局对象,包含所有浏览器 API
document.getElementById('app');           // 通过 ID 获取页面元素(Node.js 中不可用)
localStorage.setItem('key', 'value');     // 浏览器本地存储(Node.js 中不可用)

Node.js 环境示例:

实例

// Node.js 中的全局对象和文件操作
console.log(global);                              // Node.js 全局对象(对应浏览器中的 window)
const fs = require('fs');                         // 引入内置文件系统模块(浏览器中不可用)
fs.readFile('data.txt', 'utf8', callback);        // 读取服务器本地文件(浏览器中不可用)

全局对象的差异

浏览器中的全局对象:

实例

// 浏览器环境
console.log(this === window);  // true,顶层 this 就是 window 对象
var globalVar = 'hello';
console.log(window.globalVar);  // 'hello',用 var 声明的变量会成为 window 的属性

Node.js 中的全局对象:

实例

// Node.js 环境
// 注意:Node.js 中每个文件都是一个独立的模块,模块内的 this 不等于 global
console.log(this === global);  // false(在模块作用域中,this 指向 module.exports)
var globalVar = 'hello';
console.log(global.globalVar);  // undefined,模块内的变量不会自动挂载到 global

// Node.js 中的模块作用域
console.log(this);  // {} 空对象,即 module.exports 的初始值
console.log(module.exports === this);  // true,模块内 this 指向 module.exports

Node.js 特有的全局变量:

实例

console.log(__dirname);   // 当前模块所在目录的绝对路径,例如:/home/user/project
console.log(__filename);  // 当前模块文件的绝对路径,例如:/home/user/project/app.js
console.log(process);     // 进程对象,可读取命令行参数(process.argv)、环境变量(process.env)等
console.log(Buffer);      // 用于处理二进制数据的构造函数,常用于文件读写和网络通信

Node.js 的优势与局限

优势详解

1. 高并发处理能力

传统多线程服务器处理 10,000 个并发连接需要约 20GB 内存(每个线程约 2MB),而 Node.js 只需要很少的内存就能处理相同数量的连接。

实例

// Node.js 高并发示例
const http = require('http');

const server = http.createServer((req, res) => {
    // 模拟异步操作(如查询数据库),100ms 后返回响应
    // 在这 100ms 内,事件循环可以继续处理其他请求,不会被阻塞
    setTimeout(() => {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Hello World\n');
    }, 100);
});

server.listen(3000, () => {
    console.log('服务器运行在 http://localhost:3000/');
});

// 得益于非阻塞 I/O 和事件循环机制,这个服务器可以同时处理数千个请求而不会阻塞

2. 快速开发

统一的 JavaScript 语言栈(前端和后端都用 JavaScript)使得前后端开发更加高效。开发者只需掌握一门语言,就可以同时编写前端和后端代码,并且可以在两端之间共享代码逻辑:

实例

// 同一份验证逻辑可以在前后端复用,无需重复编写
function validateEmail(email) {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
}

// 前端使用(浏览器中运行):用户提交表单时先在前端校验,避免无效请求发到服务器
if (validateEmail(userInput)) {
    // 验证通过,发送到服务器
}

// 后端使用(Node.js 中运行):服务器再次校验,防止绕过前端直接请求接口
if (validateEmail(req.body.email)) {
    // 验证通过,保存到数据库
}

3. 统一语言栈的优势

  • 代码复用:前后端可以共享工具函数、数据验证逻辑、常量定义等
  • 团队效率:开发人员可以同时负责前后端开发,减少沟通成本
  • 技术栈简化:只需掌握 JavaScript 一门语言,降低学习成本和技术复杂度

4. 丰富的 npm 生态系统

# npm 提供了数百万个开源包,几乎任何功能都能找到现成的库
npm search express     # 搜索名为 express 的包
npm install express    # 安装 express 包到当前项目
npm update            # 更新当前项目中所有包到最新兼容版本

局限性分析

1. CPU 密集型任务的性能问题

实例

// 不好的例子:CPU 密集型任务会阻塞事件循环
// fibonacciSync 是纯计算任务,会一直占用 CPU,期间事件循环无法处理其他请求
function fibonacciSync(n) {
    if (n < 2) return n;
    return fibonacciSync(n - 1) + fibonacciSync(n - 2);
}

// 这会阻塞整个应用长达数秒,期间所有请求都无法响应
console.log(fibonacciSync(40));

// 解决方案:使用 Worker Threads 将 CPU 密集型任务放到独立线程中执行,
// 主线程的事件循环就不会被阻塞
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
    // 主线程:创建一个 Worker,并将参数 { n: 40 } 传给它
    // Worker 会在独立线程中执行当前这个文件(__filename 指当前文件路径)
    const worker = new Worker(__filename, {
        workerData: { n: 40 }
    });
   
    // 监听 Worker 发来的消息(计算结果)
    worker.on('message', (result) => {
        console.log('结果:', result);  // Worker 计算完成后,主线程在这里接收结果
    });
} else {
    // Worker 线程:通过 workerData 获取主线程传入的参数,执行计算后发送结果
    const result = fibonacciSync(workerData.n);
    parentPort.postMessage(result);  // 将结果发送回主线程
}

2. 回调地狱问题

实例

// 回调地狱示例:多个异步操作需要顺序执行时,回调会不断嵌套,代码可读性极差
fs.readFile('file1.txt', (err, data1) => {
    if (err) throw err;
    fs.readFile('file2.txt', (err, data2) => {   // 嵌套第一层
        if (err) throw err;
        fs.readFile('file3.txt', (err, data3) => {   // 嵌套第二层
            if (err) throw err;
            // 实际项目中这种嵌套可能有十几层,严重影响代码维护性
            console.log('所有文件读取完成');
        });
    });
});

// 现代解决方案:使用 Promise + async/await,让异步代码写起来像同步代码一样直观
const fsPromises = require('fs').promises;

async function readFiles() {
    try {
        // await 会等待每个文件读取完成,但整个函数不会阻塞事件循环
        // 注意:这里是顺序读取,如需并发读取多个文件,可以使用 Promise.all()
        const data1 = await fsPromises.readFile('file1.txt');
        const data2 = await fsPromises.readFile('file2.txt');
        const data3 = await fsPromises.readFile('file3.txt');
        console.log('所有文件读取完成');
    } catch (err) {
        // try/catch 可以统一处理所有异步错误,不需要在每个回调里单独判断
        console.error('读取文件失败:', err);
    }
}

3. 单线程的脆弱性

实例

// 未捕获的异常会导致整个 Node.js 进程崩溃
// 与多线程不同,单线程中一个未处理的错误会让整个服务停止
setTimeout(() => {
    throw new Error('未捕获的异常');  // 这会导致整个应用崩溃,所有请求都无法响应
}, 1000);

// 解决方案:注册全局异常处理器,在应用崩溃前记录日志并优雅地退出
process.on('uncaughtException', (err) => {
    console.error('未捕获的异常:', err);
    // 此处应先记录错误日志,再退出,避免静默崩溃导致问题难以排查
    process.exit(1);  // 退出码 1 表示异常退出
});

process.on('unhandledRejection', (reason, promise) => {
    // 当 Promise 被 reject 但没有 .catch() 处理时触发
    console.error('未处理的 Promise 拒绝:', reason);
    process.exit(1);
});

适用场景分析

1、RESTful API 服务

实例

// 使用 Express 框架搭建 RESTful API 服务
// 首先需要安装 express:npm install express
const express = require('express');
const app = express();

app.use(express.json());  // 中间件:自动解析请求体中的 JSON 数据

// GET 请求:获取用户列表(对应 HTTP 方法 GET,路径 /api/users)
app.get('/api/users', (req, res) => {
    const users = [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' }
    ];
    res.json(users);  // 以 JSON 格式返回数据
});

// POST 请求:创建新用户(客户端在请求体中传入用户信息)
app.post('/api/users', (req, res) => {
    const newUser = req.body;          // 获取客户端发来的 JSON 数据
    // 实际项目中这里通常会将用户信息保存到数据库
    res.status(201).json({ message: '用户创建成功', user: newUser });
});

// PUT 请求:更新指定用户信息(:id 是路由参数,如 /api/users/1 表示更新 ID 为 1 的用户)
app.put('/api/users/:id', (req, res) => {
    const { id } = req.params;         // 从路径中获取用户 ID
    const updatedData = req.body;      // 从请求体中获取更新内容
    res.json({ message: `用户 ${id} 更新成功`, data: updatedData });
});

// DELETE 请求:删除指定用户
app.delete('/api/users/:id', (req, res) => {
    const { id } = req.params;
    res.json({ message: `用户 ${id} 删除成功` });
});

app.listen(3000, () => {
    console.log('API 服务器运行在 http://localhost:3000/');
});

2、实时应用

实例

// WebSocket 实时聊天(使用 socket.io 库)
// 首先需要安装:npm install socket.io
const io = require('socket.io')(server);

// 监听客户端连接事件,每有新用户连接就触发一次
io.on('connection', (socket) => {
    // 监听当前连接的客户端发来的 'chat message' 事件
    socket.on('chat message', (msg) => {
        io.emit('chat message', msg);  // 将消息广播给所有在线客户端(包括发送者)
    });
});

3、中间件和代理服务

实例

// HTTP 反向代理服务器示例
// 将前端发来的 /api/xxx 请求转发到后端真实 API 服务器,隐藏真实地址,常用于解决跨域问题
// 首先需要安装:npm install http-proxy-middleware
const httpProxy = require('http-proxy-middleware');

const proxy = httpProxy({
    target: 'http://api.example.com',  // 真实后端 API 服务器地址
    changeOrigin: true,                // 修改请求头中的 Host 为 target 地址
    pathRewrite: {
        '^/api': ''                    // 将路径中的 /api 前缀去掉再转发,例如 /api/users → /users
    }
});