Skip to content

第6章:Node.js 异步编程

6.1 同步与异步的区别

同步编程

同步编程是指代码按照顺序执行,前一个操作完成后,才会执行下一个操作。如果前一个操作耗时较长,会阻塞后续代码的执行。

特点

  • 代码执行顺序与编写顺序一致
  • 容易理解和调试
  • 可能会阻塞程序执行

示例

javascript
// 同步操作
console.log('开始');

// 模拟耗时操作
function sleep(ms) {
  const start = Date.now();
  while (Date.now() - start < ms) {
    // 空循环,模拟耗时操作
  }
}

sleep(2000); // 阻塞 2 秒

console.log('结束');

输出结果:

开始
// 等待 2 秒
结束

异步编程

异步编程是指代码不按照顺序执行,前一个操作开始后,不会等待其完成,而是继续执行后续代码。当异步操作完成时,会通过回调函数、Promise 或 async/await 通知主线程。

特点

  • 代码执行顺序与编写顺序不一定一致
  • 不会阻塞程序执行
  • 提高程序的并发性能
  • 代码复杂度增加

示例

javascript
// 异步操作
console.log('开始');

// 模拟异步操作
setTimeout(() => {
  console.log('异步操作完成');
}, 2000);

console.log('结束');

输出结果:

开始
结束
// 等待 2 秒
异步操作完成

6.2 异步编程的三种方式

1. 回调函数

回调函数是最基础的异步编程方式,将回调函数作为参数传递给异步函数,当异步操作完成时,调用回调函数。

示例

javascript
const fs = require('fs');

// 异步读取文件(使用回调函数)
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取文件失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

console.log('读取文件操作已开始');

2. Promise

Promise 是 ES6 引入的异步编程解决方案,用于解决回调地狱问题。

基本用法

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

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

console.log('读取文件操作已开始');

创建 Promise

javascript
// 创建 Promise
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功');
    } else {
      reject('操作失败');
    }
  }, 1000);
});

// 使用 Promise
promise
  .then(result => {
    console.log('成功:', result);
  })
  .catch(error => {
    console.error('失败:', error);
  });

3. async/await

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

基本用法

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

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

readFile();
console.log('读取文件操作已开始');

注意

  • async 函数返回一个 Promise
  • await 只能在 async 函数中使用
  • await 会等待 Promise 解决(resolve)或拒绝(reject)

6.3 回调地狱及其解决方案

回调地狱

回调地狱是指多层嵌套的回调函数,使代码难以阅读和维护。

示例

javascript
// 回调地狱
fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) {
    console.error('读取 file1.txt 失败:', err);
    return;
  }
  console.log('file1.txt 内容:', data1);
  
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) {
      console.error('读取 file2.txt 失败:', err);
      return;
    }
    console.log('file2.txt 内容:', data2);
    
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) {
        console.error('读取 file3.txt 失败:', err);
        return;
      }
      console.log('file3.txt 内容:', data3);
    });
  });
});

解决方案

1. 使用 Promise 链式调用

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

// 使用 Promise 链式调用
fs.readFile('file1.txt', 'utf8')
  .then(data1 => {
    console.log('file1.txt 内容:', data1);
    return fs.readFile('file2.txt', 'utf8');
  })
  .then(data2 => {
    console.log('file2.txt 内容:', data2);
    return fs.readFile('file3.txt', 'utf8');
  })
  .then(data3 => {
    console.log('file3.txt 内容:', data3);
  })
  .catch(err => {
    console.error('读取文件失败:', err);
  });

2. 使用 async/await

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

// 使用 async/await
async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    console.log('file1.txt 内容:', data1);
    
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('file2.txt 内容:', data2);
    
    const data3 = await fs.readFile('file3.txt', 'utf8');
    console.log('file3.txt 内容:', data3);
  } catch (err) {
    console.error('读取文件失败:', err);
  }
}

readFiles();

6.4 异步实操案例

案例1:文件读取的异步实现

使用回调函数

javascript
const fs = require('fs');

// 回调函数版本
function readFileCallback() {
  console.log('开始读取文件');
  
  fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
      console.error('读取文件失败:', err);
      return;
    }
    console.log('文件内容:', data);
    console.log('读取文件完成');
  });
  
  console.log('读取文件操作已启动');
}

readFileCallback();

使用 Promise

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

// Promise 版本
function readFilePromise() {
  console.log('开始读取文件');
  
  fs.readFile('example.txt', 'utf8')
    .then(data => {
      console.log('文件内容:', data);
      console.log('读取文件完成');
    })
    .catch(err => {
      console.error('读取文件失败:', err);
    });
  
  console.log('读取文件操作已启动');
}

readFilePromise();

使用 async/await

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

// async/await 版本
async function readFileAsyncAwait() {
  console.log('开始读取文件');
  
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('文件内容:', data);
    console.log('读取文件完成');
  } catch (err) {
    console.error('读取文件失败:', err);
  }
  
  console.log('读取文件操作已启动');
}

readFileAsyncAwait();

案例2:接口请求的异步实现

使用 axios 发送 HTTP 请求

bash
npm install axios

使用 Promise

javascript
const axios = require('axios');

// Promise 版本
function fetchDataPromise() {
  console.log('开始请求数据');
  
  axios.get('https://api.example.com/data')
    .then(response => {
      console.log('请求成功:', response.data);
    })
    .catch(error => {
      console.error('请求失败:', error);
    });
  
  console.log('请求操作已启动');
}

fetchDataPromise();

使用 async/await

javascript
const axios = require('axios');

// async/await 版本
async function fetchDataAsyncAwait() {
  console.log('开始请求数据');
  
  try {
    const response = await axios.get('https://api.example.com/data');
    console.log('请求成功:', response.data);
  } catch (error) {
    console.error('请求失败:', error);
  }
  
  console.log('请求操作已启动');
}

fetchDataAsyncAwait();

6.5 新手易错点:异步代码执行顺序误区

误区1:认为异步代码会立即执行

javascript
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

console.log('3');

// 输出:1 3 2

误区2:在异步函数外使用 await

javascript
// 错误:在异步函数外使用 await
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);

// 正确:在异步函数内使用 await
async function readFile() {
  const data = await fs.readFile('example.txt', 'utf8');
  console.log(data);
}

readFile();

误区3:忘记处理 Promise 错误

javascript
// 错误:忘记处理错误
fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log(data);
  });

// 正确:处理错误
fs.readFile('example.txt', 'utf8')
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });

误区4:在循环中使用异步操作

javascript
// 错误:循环中的异步操作
const files = ['file1.txt', 'file2.txt', 'file3.txt'];

for (let i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf8', (err, data) => {
    if (err) {
      console.error(err);
      return;
    }
    console.log(`文件 ${files[i]} 内容:`, data);
  });
}

// 正确:使用 async/await 和 for...of 循环
async function readFiles() {
  const files = ['file1.txt', 'file2.txt', 'file3.txt'];
  
  for (const file of files) {
    try {
      const data = await fs.readFile(file, 'utf8');
      console.log(`文件 ${file} 内容:`, data);
    } catch (err) {
      console.error(err);
    }
  }
}

readFiles();

小结

  • 同步编程会阻塞程序执行,异步编程不会
  • Node.js 中异步编程有三种方式:回调函数、Promise、async/await
  • 回调地狱是多层嵌套的回调函数,使代码难以维护
  • Promise 和 async/await 是解决回调地狱的有效方法
  • async/await 是最简洁、最易读的异步编程方式
  • 新手需要注意异步代码的执行顺序,避免常见误区

现在,你已经了解了 Node.js 的异步编程,接下来让我们学习 HTTP 服务器开发。

© 2026 编程马·菜鸟教程 版权所有