Node.js 异步编程

在传统的同步(Synchronous)编程中,代码是按顺序执行的,每一行代码必须等待前一行代码执行完成后才能运行。而异步编程(Asynchronous)则允许代码在等待某些操作(如 I/O 操作)完成的同时继续执行其他任务,不会阻塞程序的运行。

Node.js 采用异步 I/O 模型,这使得它特别适合处理高并发的网络应用。理解异步编程是掌握 Node.js 的关键。


为什么 Node.js 需要异步编程

Node.js 是单线程的,这意味着它一次只能执行一个任务。如果使用同步编程方式,当一个耗时操作(如读取大文件或网络请求)执行时,整个程序会被阻塞,无法处理其他请求。

异步编程解决了这个问题:

  • 提高应用程序的吞吐量
  • 更有效地利用系统资源
  • 提供更好的用户体验

Node.js 异步编程的三种主要方式

1、回调函数 (Callbacks)

回调函数是 Node.js 中最基础的异步处理方式。它通过将一个函数作为参数传递给另一个函数,在操作完成后调用这个函数。

实例

const fs = require('fs');

// 异步读取文件
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取文件出错:', err);
    return;
  }
  console.log('文件内容:', data);
});

console.log('读取文件请求已发送,继续执行其他代码...');

特点

  • 简单直接
  • 容易导致"回调地狱"(Callback Hell)
  • 错误处理不够直观

2、Promise

Promise 是 ES6 引入的异步编程解决方案,它表示一个异步操作的最终完成或失败及其结果值。

实例

const fs = require('fs').promises;

// 使用 Promise 读取文件
fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log('文件内容:', data);
  })
  .catch(err => {
    console.error('读取文件出错:', err);
  });

console.log('读取文件请求已发送,继续执行其他代码...');

特点

  • 链式调用,避免回调地狱
  • 更好的错误处理机制
  • 状态不可逆(pending → fulfilled 或 rejected)

3、async/await

async/await 是 ES8 引入的语法糖,基于 Promise,使异步代码看起来像同步代码。

实例

const fs = require('fs').promises;

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('文件内容:', data);
  } catch (err) {
    console.error('读取文件出错:', err);
  }
}

readFile();
console.log('读取文件请求已发送,继续执行其他代码...');

特点

  • 代码更简洁,更易读
  • 错误处理使用 try/catch 结构
  • 必须用在 async 函数中

错误处理最佳实践

回调函数中的错误处理

实例

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取文件出错:', err);
    return; // 重要:处理错误后返回,避免执行后续代码
  }
  console.log('文件内容:', data);
});

Promise 的错误处理

实例

fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log('文件内容:', data);
    return someOtherAsyncOperation(data);
  })
  .then(result => {
    console.log('第二步结果:', result);
  })
  .catch(err => {
    console.error('处理过程中出错:', err);
  });

async/await 的错误处理

实例

async function processData() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    const result = await someOtherAsyncOperation(data);
    console.log('最终结果:', result);
  } catch (err) {
    console.error('处理过程中出错:', err);
  }
}

processData();

性能考虑

  1. 避免阻塞事件循环:长时间运行的同步代码会阻塞整个应用
  2. 合理使用异步:不是所有操作都需要异步,简单的计算任务使用同步方式可能更高效
  3. 控制并发量:过多的并行异步操作可能导致资源耗尽
  4. 使用流处理大文件:对于大文件,使用流(Stream)而不是一次性读取