How to escape async/await hell
dwqs opened this issue · 8 comments
为避免不必要的误解,本文标题由「避免陷入 async/await 地狱」改为 「How to escape async/await hell」 -- 2018/05/20 09:50
async/await 是 ES7 的新语法。在 async/await 标准出来之前,JavaScript 的异步编程经历了 callback --> promise --> generator 的演变过程。在 callback 的时代,最让人头疼的问题就是回调地狱(callback hell)。所以,在 async/await 一经推出,社区就有人认为「这是 JavaScript 异步编程的终极解决方案」。
但 async/await 也可能带来新的问题。
最近阅读了 Aditya Agarwal 的一篇文章:How to escape async/await hell。这篇文章主要讨论了过度使用 async/await 导致的新的「地狱」问题,其已经在 Medium 上获得了 19k+ 的 Applause。
好不容易逃离了一个「地狱」,又马上陷入另一个「地狱」了。
何为 async/await 地狱
在编写异步代码时,人们总是喜欢一次写多个语句,并且在一个函数调用之前使用 await
关键字。这可能会导致性能问题,因为很多时候一个语句并不依赖于前一个语句——但使用 await
关键字后,你就需要等待前一个语句完成。
示例
假设你要写一个订购 pizza 和 drink 的脚本,代码可能是如下这样的:
(async () => {
const pizzaData = await getPizzaData() // async call
const drinkData = await getDrinkData() // async call
const chosenPizza = choosePizza() // sync call
const chosenDrink = chooseDrink() // sync call
await addPizzaToCart(chosenPizza) // async call
await addDrinkToCart(chosenDrink) // async call
orderItems() // async call
})()
这段代码开起来没什么问题,也能正常的运行。但是,这并不是一个好的实现,因为这把本身可以并行执行的任务变成了串行执行。
选择一个 drink 添加到购物车和选择一个 pizza 添加到购物车可以看作是两个任务,而这两个任务之间并没有相互依赖的关系,也没有特定的顺序执行关系。所以这两个任务是可以并行执行的,这样能提高性能。而上述代码将二者变成了串行执行,显然是降低了程序性能的。
更糟糕的例子
假设要写一个程序,根据 followers 数用来显示 Github **区用户的排名情况。
如果只是获取排名,我们可以调用 Github 官方的 Search users 接口,伪代码如下:
async function getUserRank () {
const data = await fetch(search_url);
return data;
}
getUserRank();
调用 getUserRank
函数就能获取到想要的结果。但是,你可能还要想要获取每个用户的 follower 数、email、地区和仓库等数据,而 Search users 接口并没有返回这些数据,你可能需要再去调用 Single user 接口。
然后上述代码可能被改写为:
async function getUserRank () {
const data = await fetch(search_url);
const res = [];
for(let i = 0; i < data.length; i++) {
const item = data[i];
const user = await fetch(user_url);
res.push({ ...item, ...user });
}
return res;
}
getUserRank();
运行查看结果,自己想要的数据都拿到了。但是,你发现一个问题,程序运行时间有点长,该怎么优化下呢?
其实,铺垫了这么长,就是想说明一个问题:你陷入了 async/await 的地狱。
上述代码的问题在于,获取每个用户资料的请求并不存在依赖性,就类似上文中的选择 pizza 和 drink 一样,这是可以并行执行的请求。而根据上述代码,请求都变成了串行执行,这当然会损耗程序的性能。
按照上述代码,可以看一下其异步请求的动态图:
可以看到,获取用户资料的每个请求都需要等到上一个请求完成之后才能执行,Waterfall 处于一个串行的状态。那要怎么改进这个问题呢?
既然获取每个用户资料的请求并不存在依赖性,那么我们可以先触发异步请求,然后延迟处理异步请求的结果,而不是一直等该请求完成。根据这个思路,那可能改进的代码如下:
async getUserDetails (username) {
const user = await fetch(user_url);
return user;
}
async function getUserRank () {
const data = await fetch(search_url);
const promises = []
for(let i = 0; i < data.length; i++) {
const item = data[i];
const p = getUserDetails(item.username);
promises.push(p);
}
// 更精简的代码
// const promises = data.map((item) => getUserDetails(item.username))
await Promise.all(promises).then(handleYourData);
}
getUserRank();
可以看一下异步请求的动态图:
可以看到,获取用户资料的异步请求处理不再是串行执行,而是并行执行了,这将大大提高程序的运行效率和性能。
总结
Aditya Agarwal 在其文章中也给出了怎么避免陷入 async/await 地狱的建议:
- 首先找出依赖于其他语句的执行的语句
- 然后将有依赖关系的一系列语句进行组合,合并成一个异步函数
- 最后用正确的方式执行这些函数
参考
原来并行的异步操作,写成串行的,好傻啊。:-) 这文章标题党啊
@njleonzhang 你看了英文原文吗?
@dwqs 本来没看,你一说我去看了一下,和你这篇文章基本一个意思。
按我的理解,本质上这文章就是说用promise.all去实现多个异步请求并行,这东西早被说烂了。但是作者取了个高大上的名字叫async/await地狱
,吸了一波眼球。
如果这种把并行异步写成串行的实现叫做地狱
的话,用任何技术手段(callback, promise, rxjx)都能有这样的实现。
也就是说问题很普通,解法也很普通,名字高大上,所以有标题党之嫌。
我并没有怼你的意思,我只是吐糟一下这个国外大神。
最后感谢您的高产博客文章,很多让我受益匪浅。偶有吐槽,也不针对您,请不要介意。
@njleonzhang 我不是说你怼我 文章中列出的这种现象是客观存在的 19k+ 的赞同表明很多人都认可原作者的观点 并且可能很多人之前就是把「原来并行的异步操作,写成串行的」 我相信这种现象在国内也存在不少
标题党的原因可能是我的问题 标题我是直译过来的
@dwqs 这个应该不是你的锅,这个作者应该就是这个意思。也许这个很多人并不知道(没注意)Promise.all
或者并没有意识到这一点吧。毕竟业务里一般不会一次性去拿很多的detail信息。在并行请求不多的时候,一般感觉不出来差异。但是实际上,基本所有介绍async/await
的文章对于这个用法都有明确的说明,我是不太明白这个作者一副发现新大陆的样子,冠以高大上的名字, 还有这么多点赞是什么意思, 233333333.
随便google下:
阮老师的async 函数的含义和用法
体验异步的终极解决方案-ES7的Async/Await
赞同 @njleonzhang 的观点。。这本质上并不是async、await的锅。。另外,forEach也可以解决文中的问题。
熟悉js异步编程思维的开发者不会这么干,async、await 用的多的都是从其他语言转过来的服务端开发者,因为习惯同步执行的代码编写风格,按着这个思路事事滥用 async、await,才有了这篇文章
@php-cpm 现在都流行这么写了啊。async、await 的代码看着还是挺舒服的。