发布于 2026-01-06 0 阅读
0

精通 JavaScript 异步编程:揭秘 async/await、Promise 等的奥秘

精通 JavaScript 异步编程:揭秘 async/await、Promise 等的奥秘

想象一下:你身处一家熙熙攘攘的咖啡馆,翘首期盼着自己最爱的咖啡。排队等候时,你注意到一个奇特的景象:人们并没有耐心等待,而是轻松地同时做着其他事情。有人在和朋友聊天,有人在看报纸,甚至还有人下起了棋——而他们的咖啡订单却在后台悄然处理。在这家咖啡馆里,每个人都能完美地享受咖啡带来的提神效果。但如果我告诉你,你信赖的编程语言 JavaScript 也能做到类似的事情呢?欢迎来到 JavaScript 的异步编程世界,在这里,代码可以同时处理多个任务,而不会出现任何卡顿。这就像一家咖啡馆,你的代码可以与数据库交互、读取文件、从网络获取数据——所有这些都能带来流畅的用户体验。

介绍:

在用户期望值空前高的现代 Web 开发领域,同时处理多个任务的能力至关重要。JavaScript 中的异步编程应运而生,它彻底改变了我们编写 Web 代码的方式。异步编程的核心在于,它允许 JavaScript 代码高效地管理那些可能需要一些时间才能完成的任务,例如从 API 获取数据、读取文件或执行数据库操作,所有这些操作都不会阻塞主执行线程。

本文将带您揭开 JavaScript 异步编程的神秘面纱。我们将深入探讨 async/await 函数和可靠的 Promise 这两大强大组合,详细了解它们的工作原理和实际应用。无论您是经验丰富的 JavaScript 开发者,希望精进技能,还是渴望掌握基础知识的新手,都能从本文中获得宝贵的见解,助您掌握异步编程的精髓。

所以,拿起你最喜欢的饮料,舒舒服服地坐下来,准备好释放 JavaScript 异步功能的真正潜力吧。读完本文,你将掌握足够的知识和信心,轻松驾驭 async/await 的魔力,掌握 Promise 的使用,并轻松应对 JavaScript 异步编程的各种细节。

什么是异步编程?

异步编程是一种编程范式和技术,它允许任务或操作独立且并发地执行,无需等待前一个任务完成即可开始下一个任务。在异步编程中,程序可以启动一个任务,然后继续执行其他任务,而不会阻塞或等待初始任务完成。这种方法在某些任务可能需要花费不同时间才能完成的场景中尤为重要,例如网络请求、文件 I/O 操作或用户交互。通过异步执行这些任务,程序可以保持响应迅速且高效,确保在等待耗时操作完成时不会出现卡顿或无响应的情况。

在 Javascript 中实现异步编程

JavaScript 尤其大量使用异步编程,利用 Promise、async/await 和回调函数等机制来管理异步任务,从而保持流畅的用户体验。我将首先介绍 Promise,然后介绍 async/await,最后再回到 Promise,并举例说明一些使用场景。

承诺

Promise 是一个代理,代表一个在创建时不一定已知的值。它允许你将处理程序与异步操作最终的成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 Promise,该 Promise 会在未来某个时间点提供该值。Promise
处于以下几种状态之一:
⦁ Pending:初始状态,既非 fulfilled 也非 reject。⦁
Fulfilled:表示操作已成功完成。⦁
Rejected:表示操作失败。

待处理的 Promise 的最终状态可以是已完成(并赋予一个值)或已拒绝(并赋予一个原因/错误)。当出现这两种情况中的任何一种时,都会调用 Promise 的 `.then` 方法排队的相应处理程序。如果在附加相应处理程序时 Promise 已被完成或已拒绝,则会调用该处理程序,因此异步操作完成与其处理程序附加之间不存在竞争条件。如果 Promise 已被完成或已拒绝,则称其已结算,而不是处于待处理状态。

首先,我们使用 new 关键字创建一个 Promise 对象。

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("resolved");
  }, 300);
});
Enter fullscreen mode Exit fullscreen mode

Promise 构造函数接收一个带有两个参数的回调函数,第一个参数在 Promise 被成功执行时调用,第二个参数在 Promise 被拒绝时调用(注意,参数的命名是为了提高代码可读性,您可以根据自己的喜好进行命名)。这两个参数中,您要传递的消息是 Promise 成功执行或被拒绝时接收到的消息。

现在我们来看链式 Promise。`Promise.prototype.then
()`、`Promise.prototype.catch()` 和 `Promise.prototype.finally()` 方法用于将后续操作与已完成的 Promise 关联起来。由于这些方法返回的是 Promise 对象,因此可以链式调用。也就是说,`.then()` 方法会返回一个新生成的 Promise 对象,该对象可以用于链式调用;例如:

myPromise
  .then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);
Enter fullscreen mode Exit fullscreen mode

即使 `.then()` 缺少返回 Promise 对象的回调函数,处理过程也会继续执行到链中的下一个环节。因此,链可以安全地省略所有拒绝回调函数,直到最后一个 `.catch()` 和 `.finally()`。所以我们可以这样写:

myPromise
.then((value)=> console.log(`${value} is in then`))
.catch((error)=> console.log(`Error message: ${error}`))
.finally(()=> console.log('I am here no matter what happened'))
Enter fullscreen mode Exit fullscreen mode

这张图形象地解释了承诺的含义。

图片描述
此外,我们还有 Promise 并发,它赋予了 Promise 更强大的功能,这将在本文的后面部分进行讨论。

异步/等待

异步函数声明会将一个新的异步函数绑定到给定的名称。`await` 关键字允许在函数体中使用,这使得基于 Promise 的异步行为能够以更简洁的方式编写,并避免显式配置 Promise 链。简而言之,`async/await` 帮助我们编写出比 Promise 链更简洁的代码。请比较以下代码与 Promise 链:

const callMyPromise = async()=>{
try{
return await myPromise
}catch(err){
return err
}
}
Enter fullscreen mode Exit fullscreen mode

我们将在本文的应用部分探讨更复杂、更实际的 async/await 用例。
请注意:
⦁ await 仅对 Promise 有效。如果函数不返回 Promise,则使用 await 无效,即使它不会破坏您的应用程序,但为了代码库的可读性,请避免使用。⦁
await 关键字既可以在 async 函数内部使用,也可以在模块的顶层使用。
以下是一个顶层 await 的示例。

 // create a file named promise.js 
// in your promise.js you can have this:
const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("resolved");
    }, 300);
  });
  module.exports = myPromise;
Enter fullscreen mode Exit fullscreen mode
// Create another file named top_level.mjs; .mjs simply means modularized javascript file
await Promise.resolve(console.log("Top level await!!"))
Enter fullscreen mode Exit fullscreen mode

一个实际的应用场景是,当你想在 .mjs 文件中加载一个返回 Promise 的文件时:


const result =  await import ("./promise.js");
Enter fullscreen mode Exit fullscreen mode

异步编程的应用案例

  1. 发起网络请求:当您想向服务器或其他服务发起网络请求时,请求响应通常需要几微秒甚至几秒的时间,而您的应用程序依赖于从网络请求中接收的数据。我们可以通过使用 JavaScript 的 Fetch API 发起简单的 GET 请求来模拟这种情况。
//Make a request with Promise chain
const fetchWithPromiseChain = ()=>{
    const response =  fetch("https://www.boredapi.com/api/activity");
    response.then(response=> response.json()).then(data=> console.log(data)).catch(err => 

}

fetchWithPromiseChain()

// Make request with async await
const fetchWithAsyncAwait = async ()=> {
  try {
    const response = await fetch("https://www.boredapi.com/api/activity");
    const activity = await response.json();
    console.log(activity);
  } catch (e) {
    console.log("ERROR",e.message);
  }
}
fetchWithAsyncAwait();

Enter fullscreen mode Exit fullscreen mode

从上面的代码片段可以看出,我们有两种不同的方法,async/await 看起来更简洁,但只有当你的应用程序中有很多东西需要处理时,你才会意识到这一点。

例如,当你的应用需要发出多个网络请求时,我们来深入探讨一下……让我们举例说明我们的方法。

const landingPageDataWithAsyncAwait = async () => {
  try {
    console.time("async fetching data");
    const firstCall = await fetch(
      "https://official-joke-api.appspot.com/random_joke"
    );
    const randomJoke = await firstCall.json();
    const secondCall = await fetch("https://www.boredapi.com/api/activity");
    const boredomData = await secondCall.json();
    const thirdCall = await fetch("https://randomuser.me/api/");
    const randomUser = await thirdCall.json();
    const fourthCall = await fetch("https://api.ipify.org?format=json");
    const ipData = await fourthCall.json();
    console.timeEnd("async fetching data");
    return { randomJoke, boredomData, randomUser, ipData };
  } catch (error) {
    console.log("ERROR", error.message);
  }
};

Enter fullscreen mode Exit fullscreen mode

执行此函数所需的时间

async fetching data: 3.058s
Enter fullscreen mode Exit fullscreen mode

在这种情况下,每个请求都是相互独立的。我们所做的,是在一个网络请求完成后才能发出下一个请求,这会不必要地拖慢应用程序的速度。这种方法最适合每个网络请求都依赖于来自其他请求的数据的情况……更好的方法是通过并行请求。现在我们可以讨论 Promise 的并发性了;我们有 Promise.all、Promise.allSettled、Promise.any 和 Promise.reject,它们各自都是适用的场景。

  1. Promise.all:当所有承诺都得到履行时,则执行;当任何一个承诺被拒绝时,则拒绝。

  2. Promise.allSettled:当所有承诺都已兑现时生效。

  3. Promise.any:当任何一个承诺兑现时,此承诺兑现;当所有承诺都被拒绝时,此承诺被拒绝。

  4. Promise.race:当任何一个承诺达成时,它也达成。换句话说,当任何一个承诺实现时,它也实现;当任何一个承诺被拒绝时,它也被拒绝。

在这种情况下,我们将使用 .allSettled 而不是 .all。

const landingPageDataWithPromiseChain = async () => {
  try {
    console.time("fetching data");
    const firstCall = fetch(
      "https://official-joke-api.appspot.com/random_joke"
    );
    const secondCall = fetch("https://www.boredapi.com/api/activity");
    const thirdCall = fetch("https://randomuser.me/api/");
    const fourthCall = fetch("https://api.ipify.org?format=json");

    const [randomJoke, boredomData, randomUser, userIp] =
      await Promise.allSettled([firstCall, secondCall, thirdCall, fourthCall])
        .then((res) => res.map((r) => r.value.json()))
        .catch((e) => console.log(e));
    console.timeEnd("fetching data");

    return {
      randomJoke: await randomJoke,
      boredomData: await boredomData,
      randomUser: await randomUser,
      userIp: await userIp,
    };
  } catch (error) {
    console.log(error.message);
  }
};

(async () => {
  const result = await landingPageDataWithPromiseChain();
  console.log(result);
})();
Enter fullscreen mode Exit fullscreen mode

函数执行所需时间

fetching data: 1.912s
Enter fullscreen mode Exit fullscreen mode

分别运行这些案例,从控制台记录的时间可以看出,后者比前者快,而作为一名工程师,每一微秒都至关重要。

我们需要探讨另一个用例,即在数组中执行异步操作的任务。这可能相当费脑筋,但这里有一个例子以及我们可以采取的最快方法……

假设我们有一个发送到后端的包裹数组,每个包裹包含一个商品数组,每个商品都有一个 productId 值。我们需要从数据库中获取商品的特定信息,最终目标是返回每个包裹中每个商品的详细信息。

让我们来看看我们的代码……

const products = async () => await Promise.all(
        payload.boxes.map(async (box)=>{
          const items =  Promise.all( box.items.map(async (item)=>{
          const product = await products.findOne({where:{id:item.productId}});
            return {name:product?.name,id:product?.id,categoryId:product?.categoryId,subCategoryId:product?.subCategoryId};
          })
          )
          return  new Promise((resolve)=> resolve(items)); 
        })
      ) 
Enter fullscreen mode Exit fullscreen mode

我们需要多个 Promise.all,因为我们正在处理与多维数组有关的任务,而这可以说是解决此类问题的最快方法。

异步编程的应用场景远不止这些,事实上,很多 Web API 都是基于 Promise 构建的,例如 WebRTC、WebAudio 等等。作为一名软件工程师,异步编程至关重要。我希望通过这篇文章,您能对 JavaScript 的异步编程有更深入的了解。感谢您的阅读,欢迎留言告诉我您希望我在下一篇文章中探讨哪些主题。

文章来源:https://dev.to/oluwatobi_/mastering-asynchronous-programming-in-javascript-unraveling-the-magic-of-asyncawait-promises-and-more-3lc5