JavaScript 中的异步编程:从回调到 async/await

216 阅读7分钟

JavaScript 是一门单线程语言,这意味着同一时间只能执行一个任务。然而,现代应用程序要求能够处理大量的并发操作,例如用户输入、网络请求、文件读取等。为了实现这些操作而不阻塞主线程,JavaScript 提供了 异步编程 的机制。本文将带你从 回调函数Promisesasync/await,深入理解 JavaScript 中的异步编程。


目录

  1. 何为异步?
  2. 异步编程的背景与重要性
  3. 回调函数的基本概念
  4. 回调地狱与解决方案
  5. Promises 的出现与优势
  6. async/await:简化异步编程
  7. JavaScript 异步编程的错误处理
  8. 常见异步编程陷阱与最佳实践
  9. 总结

1. 何为异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 -- XHRFetch
  • 用户操作后需要执行的任务 -- addEventListener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」

202208101043348.png

渲染主线程承担着极其重要的工作,无论如何都不能阻塞!

因此,浏览器选择异步来解决这个问题

202208101048899.png


2. 异步编程的背景与重要性

在现代 web 开发中,很多操作都需要等待一定时间才能完成,例如网络请求、数据库查询、文件读取等。如果在 JavaScript 中这些操作是同步执行的,那么它们会阻塞主线程,导致页面无法响应用户操作。这就是为什么我们需要 异步编程

异步编程的核心思想是:任务不会阻塞主线程,而是将任务放入任务队列,主线程继续执行其他操作。当任务完成时,会从队列中取出任务并执行。


3. 回调函数的基本概念

回调函数(Callback Function) 是 JavaScript 中最常见的异步编程模式。回调函数是作为参数传递给其他函数的函数,它会在某个任务完成时被调用。

回调函数示例:

function fetchData(callback) {
  setTimeout(() => {
    console.log("Data fetched");
    callback("Success");
  }, 2000);
}

fetchData((message) => {
  console.log(message); // 输出:"Success"
});

在上面的例子中,fetchData 函数接受一个回调函数 callback,当模拟的异步操作(setTimeout)完成后,调用 callback 并传入结果。


4. 回调地狱与解决方案

回调地狱(Callback Hell)

回调地狱指的是在多个嵌套回调函数中,代码变得非常难以阅读和维护,特别是当多个异步操作之间有依赖关系时。

fetchData((message) => {
  console.log(message);
  fetchData((message2) => {
    console.log(message2);
    fetchData((message3) => {
      console.log(message3);
    });
  });
});

这段代码虽然能正确执行,但当任务较多时,会使得代码层级变得很深,阅读和维护起来非常困难。

解决方案:使用 Promises


5. Promises 的出现与优势

Promise 是一种新的异步编程机制,用于替代回调函数。它提供了更清晰的语法,使得异步操作更加可读、可维护。Promise 代表一个异步操作的最终结果,它可以处于以下三种状态:

  • Pending(待定) :初始状态,操作未完成。
  • Fulfilled(已完成) :操作成功完成,Promise 有结果值。
  • Rejected(已拒绝) :操作失败,Promise 有错误原因。

Promise 示例:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true; // 假设操作成功
      if (success) {
        resolve("Data fetched successfully");
      } else {
        reject("Error fetching data");
      }
    }, 2000);
  });
}

fetchData()
  .then((message) => {
    console.log(message); // 输出:"Data fetched successfully"
  })
  .catch((error) => {
    console.error(error); // 如果失败,输出错误
  });

通过 then 方法,可以链式调用多个异步操作;catch 方法用于处理错误。相较于回调函数,Promise 使得代码更加简洁、可读,避免了回调地狱的问题。


6. async/await:简化异步编程

async/await 是基于 Promise 的语法糖,用来进一步简化异步代码。它使得异步代码看起来像同步代码一样,从而提高了代码的可读性。

  • async:用于声明一个异步函数,返回一个 Promise
  • await:用于等待一个 Promise 完成,它只在 async 函数内有效。

async/await 示例:

async function fetchData() {
  const response = await new Promise((resolve, reject) => {
    setTimeout(() => resolve("Data fetched successfully"), 2000);
  });
  console.log(response);
}

fetchData(); // 输出:"Data fetched successfully"

在上述例子中,await 会暂停函数的执行,直到 Promise 完成。这样,我们避免了使用 .then() 进行链式调用,代码看起来更简洁。

异常处理:

async/await 使得错误处理更加简单,直接使用 try/catch 来捕获异步操作中的错误。

async function fetchData() {
  try {
    const response = await new Promise((resolve, reject) => {
      setTimeout(() => reject("Error fetching data"), 2000);
    });
    console.log(response);
  } catch (error) {
    console.error(error); // 输出:"Error fetching data"
  }
}

fetchData();

7. JavaScript 异步编程的错误处理

在异步编程中,错误处理非常重要。我们已经看到了如何在回调函数中处理错误,以及如何在 Promiseasync/await 中使用 catchtry/catch 来捕获错误。

  • 在回调函数中,错误通常通过第二个参数传递给回调。
  • Promise 中,可以通过 catch 来处理错误。
  • async/await 中,可以使用 try/catch 语法来捕获异常。

常见的错误类型:

  • 网络错误:请求失败或超时。
  • 语法错误:在异步回调中使用错误的语法。
  • 逻辑错误:例如 Promise 状态错误或数据传递不正确。

8. 常见异步编程陷阱

8.1 避免回调地狱

回调地狱通常发生在异步操作链式调用时,尤其是当多个异步操作之间有依赖关系时。为了解决这个问题,使用 Promiseasync/await 来避免嵌套过深的回调函数。

8.2 使用 Promise.all 处理并行任务

如果你有多个异步任务并行执行,Promise.all 是一个很好的选择,它能将多个 Promise 聚合成一个 Promise,等待所有任务完成后再执行下一个操作。

async function fetchData() {
  const [data1, data2] = await Promise.all([
    fetch("url1").then(res => res.json()),
    fetch("url2").then(res => res.json())
  ]);
  console.log(data1, data2);
}

8.3 使用 finally 来清理操作

finally 方法允许你在 Promise 完成后无论成功与否都执行某些操作。例如,用于清理操作(如关闭文件、清理缓存等)。

fetchData()
  .finally(() => {
    console.log("Clean up tasks");
  });

9. 总结

JavaScript 的异步编程使得我们可以高效地处理 I/O 操作,避免主线程阻塞,保持用户体验流畅。从回调函数到 Promise 再到 async/await,JavaScript 提供了越来越方便的方式来编写异步代码。