我保证你很快就能理解 JavaScript 中的异步

2025-06-10

我保证你很快就能理解 JavaScript 中的异步

当你探索现代 JavaScript 时,很快就会遇到主要的异步关键字之一:Promiseawaitasync。那么,它们是如何工作的?你为什么要使用它们?(最后,还有一些充分利用它们的专业技巧。)

与异步编程中的所有事物一样,我们最终会回答这些问题,但我们回答的顺序尚未确定。

async function writeBlogPost() {
  await Promise.all([
    writeHowAsyncWorks(),
    writeWhyAsync().then(() => writeAsyncIsNotMultithreading())
  ])
    .then(() => writeProTips())
    .finally(() => writeConclusion());
}
Enter fullscreen mode Exit fullscreen mode

为什么是异步?

JavaScript 自诞生之日起就存在于互联网中。这意味着它必须处理一些耗时不确定的任务(通常是从你的设备向某个服务器发出的调用)。JavaScript 传统上处理这类任务的方式是“回调”:

function getImageAndDoSomething() {
  // This is a simplified example, of course, since arrow functions
  // didn't exist back in the day...
  loadDataFromSite(
    // Function argument 1: a URL
    "http://placekitten.com/200/300",
    // Function argument 2: a callback
    (image, error) => {
      // Do something with `image`
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

回调函数是指在工作完成后调用的函数。上面的函数会在成功从目标 URL 加载数据后,loadDataFromSite调用带有定义参数的回调函数。如果加载失败,则会调用带有定义参数的回调函数,并将图像设置为定义参数。imagenullerror

当你处理简单的“获取并执行一件事”循环时,这种方法没问题。但是,如果你需要对服务器进行多次链式调用,这种方法很快就会陷入回调地狱:

function apiCallbackHell() {
  loadData((data, error) => {
    data && transformData(data, (transformed, error) => {
      transformed && collateData(transformed, (collated, error) => {
        collated && discombobulateData(collated, (discombobulated, error) => {
          // And so on...
        })
      })
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

这真是一团糟!像这样的回调地狱正是Promise API背后的动机,而 Promise API 又催生了async/await API。稍后我们会详细分析它的作用,但现在我们先来欣赏一下使用 async/await 后函数看起来多么简洁

async function notApiCallbackHell() {
  const data = await loadData();
  const transformed = await transformData(data);
  const collated = await collateData(transformed);
  const discombobulated = await discombobulateData(collated);
  // And so on...
}
Enter fullscreen mode Exit fullscreen mode

支线任务:异步不是多线程 JavaScript

不过,在我们深入分析之前,我们先来澄清一个常见的误解:异步代码与多线程代码不同。JavaScript 的核心仍然是单线程环境。

该语言的底层有一个叫做“事件循环”的东西,它是负责读取并执行单个指令的引擎。该循环仍然是一个单线程进程——它每次只能读取一条指令,然后继续执行。

回调和 Promise 让这个循环看起来像是在同时执行多件事,但事实并非如此。我们可以把代码中的指令想象成一堆牌,而事件循环就像一个发牌人,每次从最上面抽出一张牌,然后把它们叠成一副整齐的牌。如果没有回调或 Promise,那么发牌人能抽出的牌堆就很明确了:它就是程序里的东西,从上到下逐行读取代码。

添加异步代码后,我们的发牌人可以从另一个堆中读取数据——回调或 Promise 中的代码可以独立于程序全局范围内的指令读取。然而,仍然只有一个发牌人(一个线程),并且他们一次只能读取一条指令。只不过现在他们的工作在不同的堆中共享了。这意味着,如果你把一些非常困难的工作放入 Promise 中,你将创建一个非常大的新堆供你的发牌人读取数据。这会减慢其他代码的执行速度,因此屏幕上的交互式 UI 可能会变得非常慢。

解决这个问题的方法是将你的繁重工作转移到另一个线程——用我们的比喻来说,这就像是雇佣第二个经销商,独立于主经销商,对堆积如山的指令进行整理。如何做到这一点超出了本文的讨论范围,但如果你对此感兴趣,可以查看Node 的工作线程浏览器的 Web Workers

这里有哪些碎片?

因此,我们已经听说过 async/await 领域中的三种主要工具,但它们实际上做什么以及它们如何工作?

承诺

async/await 工具包的核心是Promise类型。s是对象。它们包装执行某些Promise操作的代码。其最初的目的是为了更容易地将回调和错误处理程序附加到该代码。创建 Promise 的方法有很多种,但最基本的是:

new Promise((resolve, reject) => {
  // Do something
  if (itSucceeded) {
    resolve(successResult);
  } else {
    reject(failureReason);
  }
});
Enter fullscreen mode Exit fullscreen mode

在这里你可以看到 a 的核心特性Promise——它只是一个回调的包装器!在我们 new 的执行块中,Promise我们只有两个回调——一个是 Promise 成功执行时调用的回调(resolve回调),另一个是 Promise 失败时调用的回调(reject回调)。

Promise然后我们得到两个最重要的函数:

const somePromise = getPromise();

somePromise
  .then((result) => {
    // Do something with a success
  })
  .catch((rejection) => {
    // Do something with a rejection
  });
Enter fullscreen mode Exit fullscreen mode

then如果您从其他代码中catch获得了 ,则它们非常有用。您可以通过这些方法将自己的回调附加到,以监听其解析时间(在这种情况下,您的回调将使用解析值进行调用)或处理失败情况(在这种情况下,您的回调将使用拒绝原因(如果有)进行调用)。PromisePromisethencatch

(附注:finally正如您可能猜到的那样,在所有thencatch处理程序完成后,还会运行一个。)

Then 和 catch 也很有用,因为它们本身返回一个Promise包含处理程序返回值的 now。

因此,您可以使用.then将多个步骤链接在一起,部分逃避回调地狱:

function promisePurgatory() {
  loadData(data)
    .then(data => transformData(data))
    .then(transformed => collateData(transformed))
    .then(collated => discombobulateData(collated))
    .then( /* and so on */ );
}
Enter fullscreen mode Exit fullscreen mode

异步/等待

不过,你可能已经注意到,这Promise并不能完全让我们摆脱对大量回调的需求。当然,它们现在都在同一层级,所以我们不再需要无限循环。但是,JavaScript 背后的社区确信他们可以做得更好。Enterasync及其合作伙伴await。这两个工具Promise极大地简化了编程。

首先是async- 这是一个关键字,用来注释一个函数,表示它返回一个Promise。你无需做任何进一步的操作,如果你将一个函数标记为async,它现在将被视为与你将其作为 Promise 中的执行块一样。

async function doSomeWork() {
  // Do some complicated work and then
  return 42;
}

async function alwaysThrows() {
  // Oh no this function always throws
  throw "It was called alwaysThrows, what did you expect?"
}

const automaticPromise = doSomeWork();
// Without having to call `new Promise` we have one.
// This will log 42:
automaticPromise.then((result) => console.log(result));

const automaticReject = alwaysThrows();
// Even though the function throws, because it's async the throw
// is wrapped up in a Promise reject and our code doesn't crash:
automaticReject.catch((reason) => console.error(reason));
Enter fullscreen mode Exit fullscreen mode

这本身就很有用——你再也不用记住如何实例化,Promise也不用担心处理大小写rejectthrow错误了。但真正精彩的是当你添加 时await

await只能存在于async函数内部,但它提供了一种暂停函数的方法,直到其他函数Promise完成。然后,您将获得该函数的解析值Promise,或者,如果它被拒绝,则会抛出拒绝信息。这允许您Promise直接处理结果,而无需为其构建回调。这是我们真正摆脱回调地狱所需的最后一个工具:

// From above, now with error handling
async function notApiCallbackHell() {
  try {
    const data = await loadData();
    const transformed = await transformData(data);
    const collated = await collateData(transformed);
    const discombobulated = await discombobulateData(collated);
    // And so on...
  } catch {
    // Remember - if the Promise rejects, await will just throw.
    console.error("One of our ladders out of hell failed");
  }
}
Enter fullscreen mode Exit fullscreen mode

一些 Pro(mise) 技巧

Promise现在您已经对、async和 的基础知识有了await更好的了解,下面是使用它们时需要牢记的一些专业提示:

  1. async.thenPromise自动展平返回的 s。和 都async足够.then智能,知道如果您Promise为某个值返回 a ,您的最终用户并不为某个值返回 a PromisePromise您可以直接返回您的值,也可以Promise为其返回 a ,它都会被正确地展平。

  2. Promise.all用于连接,而不是多个awaits。如果你有多个Promises,它们彼此不依赖,并且你想等待它们全部完成,你的第一反应可能是这样做:

async function waitForAll() {
  // Don't do this
  const one = await doPromiseOne();
  const two = await doPromiseTwo();
  const three = await doPromiseThree();
}
Enter fullscreen mode Exit fullscreen mode

不过,这会给你带来麻烦,因为你必须等待第一个 Promise 完成后才能开始第二个 Promise,以此类推。你应该使用内置函数Promise.all

async function waitForAll() {
  const [one, two, three] = await Promise.all([
    doPromiseOne(), doPromiseTwo(), doPromiseThree()
  ]);
}
Enter fullscreen mode Exit fullscreen mode

这样,您的代码将预先创建所有三个 Promise,并同时执行它们。您仍然需要完成await所有三个 Promise,但所需时间会大大减少,因为您可以将 PromiseOne 的停机时间用于处理 PromiseTwo 或 Three。

  1. Promise.allSettled如果失败是可以接受的。Promise.all序列化s的缺点await是,如果其中一个Promises 被拒绝,那么整个链都会被拒绝。这就是它的Promise.allSettled用武之地。它的工作原理与 s 相同,Promise.all只是它会等到所有参数都解析拒绝后,再将 s 本身的数组返回给你Promise。如果你正在尝试做一些工作,但即使失败也没关系,这很有用。

  2. 箭头函数也可以async最后但同样重要的是,请务必记住,箭头函数async也可以标记为!如果您要创建一个需要使用 的回调处理程序await,例如用于onSubmit表单的 ,这真的非常有用:

// Imagining we're in react...
return <Form onSubmit={
  async (values) => {
    const serverResponse = await submitValuesToServer(values);
    window.location.href = "/submitted/success";
  }
}>{/* Form contents */}</Form>
Enter fullscreen mode Exit fullscreen mode

。最后(...)

在下面的评论中告诉我你现在对Promiseasync和有什么疑问await。尽管我在编写的每个 Node 和 React 应用中都使用了这三个,但它们之间仍然有很多细微的差别需要学习。

如果您喜欢这篇文章,请给我点个赞,或者可以看看我上一篇关于JS细节的this“回归基础”文章。

鏂囩珷鏉yu簮锛�https://dev.to/zackdotcomputer/i-promise-you-won-t-have-to-await-long-to-understand-async-in-javascript-27ab
PREV
每个开发人员都需要的 4 种非编码技能
NEXT
Jetpack Compose 中的作用域重组——状态改变时会发生什么?