为什么我避免使用async/await?

2022-06-07 20:44:38 浏览数 (2)

Yodonicc

无论你对async/await的立场如何,我都想向你说明,根据我的经验,为什么async/await往往会使代码复杂度更高,而不是更低。

JavaScript中的async/await功能的效用是基于这样的想法:异步代码很难,相比之下,同步代码更容易。这在客观上是正确的,但在大多数情况下,我不认为async/await真的能解决这个问题。

谎言和async/await

我用来确定是否要使用某个模式的指标之一是它所带来的代码综合质量。例如,一个模式可能是干净的、简洁的或广泛使用的,但如果它导致了容易出错的代码,它就是一个我可能会拒绝的模式。这些模式是双刃剑,很容易搬起石头砸自己的脚。

首先,它是建立在一个谎言之上的。

Async/await让你的异步代码看起来像同步的一样。

这是它的卖点。但对我来说,这就是问题所在。它从一开始就为你的代码所发生的事情设定了错误的心理模型。同步代码可能比异步代码更容易处理,但同步代码不是异步代码。它们有非常不同的属性。

很多时候这不是问题,但当它是问题时,就很难识别,因为async/await正好隐藏了显示它的线索。以这段代码为例。

同步代码

代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) => {
  save('userData', userData);
  save('session', sessionPrefences);
  return { userData, sessionPrefences }
}

Async/Await

代码语言:javascript复制
const processData = async ({ userData, sessionPrefences }) => {
  await save('userData', userData);
  await save('session', sessionPrefences);
  return { userData, sessionPrefences }
}

Promise

代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) =>   save('userData', userData)
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })

这里有一些性能问题。我们已经把问题缩小到了processData函数上。在这三种情况中,你对优化途径的假设是什么?

我看了第一种情况,发现我们在两个不同的地方保存了两块不同的数据,然后只是返回一个对象。唯一可以优化的地方是保存函数。没有任何其他选择。

我看了第二个例子,也有同样的想法。唯一可以优化的地方是保存函数。

也许只是因为我对Promise的太熟悉了,但我看了第三个例子,我很快看到了一个机会。我看到我们在连续调用save,尽管其中一个并不依赖于另一个。 我们可以将我们的两个save调用并行化。

代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) => Promise.all([
  save('userData', userData),
  save('session', sessionPrefences)
])
  .then(() => ({ userData, sessionPrefences })

同样的机会也存在于async/await代码中,只是因为我们处于异步代码的思维模式中,所以它被隐藏在明处。在async/await版本中并不是没有提示。关键字async和await应该给我们同样的直觉,就像第三个版本中的then一样。但我敢打赌,对许多工程师来说,它并没有。

为什么没有呢?

这是因为我们被教导要以同步的思维方式来阅读async/await代码。在第一个同步代码例子中,我们无法将保存调用并行化,同样的逻辑(但现在是不正确的),我们来到第二个例子。Async/await将我们的思维置于同步的思维模式中,而这是错误的思维模式。

此外,如果我们要在async/await的例子中利用并行化的优势,无论如何我们必须使用promise。

代码语言:javascript复制
const processData = async ({ userData, sessionPrefences }) => {
  await Promise.all([
    save('userData', userData), 
    save('session',sessionPrefences)
  ])
  return { userData, sessionPrefences }
}

在我看来,如果一个特定的模式只在一些常见的情况下工作,那么它一定有一些非常大的优势。如果我不得不在一些非常常见的情况下 "退回"到promise模式,那么我就看不到async/await比promise有什么优势。对我来说,在多种范式之间切换的认知负担并不值得。promise在任何情况下都能完成工作,而且每次都和async/await一样好,甚至更好。

错误处理

处理错误对于异步代码来说是至关重要的。有几个关键的地方,我们必须担心JavaScript中同步代码的错误处理。这主要发生在我们把一些东西交给本地API,如JSON.parse,或浏览器功能,如window.localstorage

让我们来看看我们之前的save函数的例子,并应用一些错误处理。让我们假设在我们的同步例子中,save执行了一个可能会抛出的操作。这是非常合理的,因为如果保存到sessionstorage,它可能在序列化或试图访问sessionstorage的过程中抛出。为了处理同步代码中可能出现的错误,我们通常使用try/catch。

同步代码
代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) => {
  try {
    save('userData', userData);
    save('session', sessionPrefences);
    return { userData, sessionPrefences }
  } catch (err) {
    handleErrorSomehow(err)
  }
}

根据不同的策略,我们可能重新抛出错误,或者在catch块中返回一些默认值。无论哪种方式,我们都必须在try块中封装任何可能抛出错误的逻辑。

async/await

由于async/await让我们 "像看待同步一样看待async代码",我们也使用try/catch块。捕获块甚至会将我们的reject判定为一个错误。

代码语言:javascript复制
const processData = async ({ userData, sessionPrefences }) => {
  try {
    await save('userData', userData);
    await save('session', sessionPrefences);
    return { userData, sessionPrefences }
  } catch (err) {
    handleErrorSomehow(err)
  }
}

看看这个,async/await实现了它的承诺。它看起来与同步版本几乎完全一样。

现在,有一些编程流派非常倚重try/catches。我觉得它们是一种精神上的负担。每当有try/catch时,我们现在不仅要担心函数返回什么,还要担心它抛出什么。我们不仅有分支逻辑,这增加了复杂性,而且还必须担心同时处理两种不同的范式。一个函数可以返回一个值,也可以抛出。因此,每个函数都要处理这两方面的问题。这很累人。

try/catch的尴尬

关于try/catch的最后一点。在JavaScript中,你一般不会在很多地方看到拥抱try/catch。与其他语言不同的是,在其他语言中,你会经常看到它,比如Java。JavaScript中的try块会立即将这部分代码排除在许多引擎优化之外,因为代码不能再被分解成确定的片段。换句话说,在JavaScript中,同样的代码在被try块包裹的情况下会比不被包裹的情况下运行得更慢,即使它没有抛出的可能性。

Promise

让我们看看Promise在做什么。

代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) =>   save('userData', userData)
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })
  .catch(handleErrorSomehow)

你看到了吗?

代码语言:javascript复制
.catch(handleErrorSomehow)

是的。这就是它的全部内容。这和其他的方法做的事情完全一样。我发现这比try/catch块更容易阅读。你觉得呢?如果同步代码也这么简单就好了......等一下!

代码语言:javascript复制
const processData = ({ userData, sessionPrefences }) => Promise.resolve(save('userData', userData))
  .then(() => save('session', sessionPrefences))
  .then(() => ({ userData, sessionPrefences })
  .catch(handleErrorSomehow)

0 人点赞