Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to escape async/await hell #65

Open
dwqs opened this issue May 19, 2018 · 8 comments
Open

How to escape async/await hell #65

dwqs opened this issue May 19, 2018 · 8 comments

Comments

@dwqs
Copy link
Owner

dwqs commented May 19, 2018

为避免不必要的误解,本文标题由「避免陷入 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 一样,这是可以并行执行的请求。而根据上述代码,请求都变成了串行执行,这当然会损耗程序的性能。

按照上述代码,可以看一下其异步请求的动态图:

images

可以看到,获取用户资料的每个请求都需要等到上一个请求完成之后才能执行,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();

可以看一下异步请求的动态图:

images

可以看到,获取用户资料的异步请求处理不再是串行执行,而是并行执行了,这将大大提高程序的运行效率和性能。

总结

Aditya Agarwal 在其文章中也给出了怎么避免陷入 async/await 地狱的建议:

  1. 首先找出依赖于其他语句的执行的语句
  2. 然后将有依赖关系的一系列语句进行组合,合并成一个异步函数
  3. 最后用正确的方式执行这些函数

参考

@njleonzhang
Copy link

njleonzhang commented May 20, 2018

原来并行的异步操作,写成串行的,好傻啊。:-) 这文章标题党啊

@dwqs
Copy link
Owner Author

dwqs commented May 20, 2018

@njleonzhang 你看了英文原文吗?

@njleonzhang
Copy link

njleonzhang commented May 20, 2018

@dwqs 本来没看,你一说我去看了一下,和你这篇文章基本一个意思。
按我的理解,本质上这文章就是说用promise.all去实现多个异步请求并行,这东西早被说烂了。但是作者取了个高大上的名字叫async/await地狱,吸了一波眼球。
如果这种把并行异步写成串行的实现叫做地狱的话,用任何技术手段(callback, promise, rxjx)都能有这样的实现。
也就是说问题很普通,解法也很普通,名字高大上,所以有标题党之嫌。
我并没有怼你的意思,我只是吐糟一下这个国外大神。

你看下面的评论也有人这么说

1526780680436

image

最后感谢您的高产博客文章,很多让我受益匪浅。偶有吐槽,也不针对您,请不要介意。

@dwqs
Copy link
Owner Author

dwqs commented May 20, 2018

@njleonzhang 我不是说你怼我 文章中列出的这种现象是客观存在的 19k+ 的赞同表明很多人都认可原作者的观点 并且可能很多人之前就是把「原来并行的异步操作,写成串行的」 我相信这种现象在国内也存在不少

标题党的原因可能是我的问题 标题我是直译过来的

@dwqs dwqs changed the title 避免陷入 async/await 地狱 How to escape async/await hell May 20, 2018
@njleonzhang
Copy link

njleonzhang commented May 20, 2018

@dwqs 这个应该不是你的锅,这个作者应该就是这个意思。也许这个很多人并不知道(没注意)Promise.all或者并没有意识到这一点吧。毕竟业务里一般不会一次性去拿很多的detail信息。在并行请求不多的时候,一般感觉不出来差异。但是实际上,基本所有介绍async/await的文章对于这个用法都有明确的说明,我是不太明白这个作者一副发现新大陆的样子,冠以高大上的名字, 还有这么多点赞是什么意思, 233333333.

随便google下:
阮老师的async 函数的含义和用法
体验异步的终极解决方案-ES7的Async/Await

@Dcatfly
Copy link

Dcatfly commented May 26, 2018

赞同 @njleonzhang 的观点。。这本质上并不是async、await的锅。。另外,forEach也可以解决文中的问题。

@php-cpm
Copy link

php-cpm commented May 28, 2018

熟悉js异步编程思维的开发者不会这么干,async、await 用的多的都是从其他语言转过来的服务端开发者,因为习惯同步执行的代码编写风格,按着这个思路事事滥用 async、await,才有了这篇文章

@njleonzhang
Copy link

njleonzhang commented May 28, 2018

@php-cpm 现在都流行这么写了啊。async、await 的代码看着还是挺舒服的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants