如何逃离 async / await 地狱
为避免不必要的误解,本文标题由「避免陷入 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 地狱的建议:
- 首先找出依赖于其他语句的执行的语句
- 然后将有依赖关系的一系列语句进行组合,合并成一个异步函数
- 最后用正确的方式执行这些函数
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 图片和视频的懒加载
下一篇: 不要相信一个熬夜的人说的每一句话
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
按我的理解,本质上这文章就是说用 promise.all 去实现多个异步请求并行,这东西早被说烂了。但是作者取了个高大上的名字叫
async/await 地狱
,吸了一波眼球。如果这种把并行异步写成串行的实现叫做
地狱
的话,用任何技术手段(callback, promise, rxjx) 都能有这样的实现。也就是说问题很普通,解法也很普通,名字高大上,所以有标题党之嫌。
我并没有怼你的意思,我只是吐糟一下这个国外大神。
你看下面的评论也有人这么说
最后感谢您的高产博客文章,很多让我受益匪浅。偶有吐槽,也不针对您,请不要介意。