一、问题
这篇文章提醒大家注意,使用 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()使它们同时发生。