【翻译】async函数的一个注意点

一、问题

这篇文章提醒大家注意,使用 JavaScript 的 async/await 函数时,要区分并发操作和继发操作。

你能看出下面代码的问题吗?

async function getPeople() {
  const members = await fetch("/members");
  const nonMembers = await fetch("/non-members");

  return members.concat(nonMembers);
}

花点时间看看。像这样的代码可能存在于很多的JavaScript代码库中。有时候,它就在那些了解他的人眼前,包括我自己。事实上,正是我所犯的这个错误促使我写下这篇文章。

我给你们一个提示。下面是没有async/await语法时的样子:

function getPeople() {
  return fetch("/members")
    .then(members => fetch("/non-members")
      .then(nonMembers => members.concat(nonMembers)))
}

你现在看到了吗?编写这个版本实际上很棘手,因为在没有异步/等待的情况下很难犯这个错误。

我们采用了两个独立的异步任务,并将它们放入一个序列中。这个函数花费的时间是它需要的时间的两倍。

它应该是这样的:

async function getPeople() {
  const members = fetch("/members");
  const nonMembers = fetch("/non-members");
  const both = await Promise.all([ members, nonMembers ]);

  return both[0].concat(both[1]);
}

或者没有async/await:

function getPeople() {
  const members = fetch("/members");
  const nonMembers = fetch("/non-members");

  return Promise.all([ members, nonMembers ])
          .then(both => both[0].concat(both[1]));
}

二、简单的错误

请注意,当“糟糕”版本被写成简单的promise时,情况要复杂得多。promise API鼓励并行性,但当需要按顺序放置东西时就会变得笨拙。当需要事情按顺序发生时Async/await就作为一种解决方案出现,比如您需要将一个请求的输出作为参数提供给下一个请求。这确实让两种情况都更清楚了。但重要的是它颠倒了它们,使顺序流比并行流更容易。

即使您理解了语法最终转换成的promise代码,也很容易落入这个陷阱。如果你还不了解promise和async/await到底是什么,就会更容易犯错。

我不会说 async/await 不好(或者“危险”)。特别是在那些没有像promise这样合理的API的语言中,或者在闭包方面确实有其他限制,使得它们使用起来不那么简单,async/await可以使代码总体上更具可读性。

但它们确实有一个很大的陷阱。这种语法的优点和缺点都在于,它允许我们把异步的东西假装它们是连续的。我们的大脑更容易推理连续的过程。

但是我认为用 async/await 编写东西的最简单、最清楚的方法往往是错误的,这是一个遗憾。而且错误的方式很微妙,可能永远不会被注意到,因为它只影响性能,而不是正确性。

我非常相信一些语言和库,这些语言和库使做通常正确的事情变得容易,做通常错误的事情变得困难。那就是说,我没有一个建议如何async/await可以做不同的。我只是后悔事情变成这样。

但至少,我认为越来越重要的是——随着越来越多的语言采用这种语法——人们意识到它是如何容易被误用的。

三、问题解决

如果您不确定代码的行为方式,请查看瀑布图,看看是否有机会让您的应用程序更快一些。

您还可以使用这条经验法则:
如果一个await的函数调用使用另一个await的函数调用的结果(或从该结果派生出的东西),那么应该使用Promise.all()使它们同时发生。