译文出自: 夜色镇歌的个人博客 https://blog.catwen.cn/2018/05/10/how-to-escape-async-await-hell/
async/await 把我们从回调地狱中解救了出来, 但是如果滥用就会掉进 async/await 地狱.
本文中我会解释一下什么是 async/await 地狱, 并会分享几个技巧去避免.
啥是 await/async 地狱
异步 Javascript 编程中, 我们通常会写许多 async 方法, 并且使用 await 关键字去等待它, 有很多时候下一行的执行并不依赖于上一行, 但是我们仍然使用了 await 去等待, 所以可能会导致一些性能问题.
一个 await/async 地狱的例子
如何编写一个订购披萨和饮料的代码? 它可能会像这样:
- (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
- })()
看起来没什么问题, 也能正常工作, 但这并不是一个好的实现. 先来看下这段代码都做了什么, 以便定位问题.
解释下
我们把代码用 async IIFE 包裹了起来, 然后下面这些会依次执行.
获取披萨菜单
获取饮料菜单
从披萨菜单中选择披萨
从饮料菜单中选择饮料
把选好的披萨加到购物车
把选好的饮料加到购物车
下单
哪里错了?
正如我刚强调的, 这些语句会依次执行, 没有并发. 仔细想一下, 为啥我获取饮料菜单之前得先获取披萨菜单? 这两份菜单我应该同时去获取. 当然, 选择披萨之前得先获取披萨菜单, 这个规则同样适用于饮料.
所以我们可以得出结论, 披萨相关的工作和饮料相关的工作可以并行进行, 但涉及披萨相关工作的各个步骤需要按顺序进行 (一步接着一步).
另一个糟糕的例子
这段代码会获取购物车中的购物项并且发出订购请求.
- async function orderItems() {
- const items = await getCartItems() // async call
- const noOfItems = items.length
- for(var i = 0; i <noOfItems; i++) {
- await sendRequest(items[i]) // async call
- }
- }
这个例子中 for 循环在下一次迭代之前必须等待上一个 sendRequest() 执行完毕, 可我们根本不需要等待, 只想尽快的把请求都发送出去然后等待他们都完成.
想必现在你已经了解了什么是 async/await 地狱, 以及它对性能的影响是多么的严重. 现在我想问你个问题.
如果忘记了 await 关键字呢?
如果忘记使用 await,async 函数会执行并且返回一个 Promise, 你可以稍后再去 resolve.
- (async () => {
- const value = doSomeAsyncTask()
- console.log(value) // an unresolved promise
- })()
另一个后果是编译器不知道你想把函数完全执行, 所以编译器会退出程序而不完成异步函数, 所以还是需要使用 await 关键字
promises 一个有趣的特性就是你可以在一行代码中去得到 Promise , 而在另外一行中去等待并 resolve, 这是避免 async/await 地狱的关键之处.
- (async () => {
- const promise = doSomeAsyncTask()
- const value = await promise
- console.log(value) // the actual value
- })()
正如你看到的, doSomeAsyncTask() 方法返回一个 Promise, 调用的时候它已经开始执行了, 为了得到他的解析值, 我们使用了 await 关键字, 告诉编译器等待解析完毕再执行下一行.
如何避免 async/await 地狱
你应该按照这些步骤来避免 async/await 地狱:
找到语句的依赖关系
第一个例子中, 我们选择了一个披萨和一杯饮料. 总结一下, 选择披萨之前得先获取披萨菜单, 加到购物车之前得先选好, 这三个步骤都是相互依赖的, 必须等待上一个步骤完成后才能进行下一步.
我们选择饮料的时候并不依赖于选择披萨, 所以选择披萨和饮料是可以并行执行的. 这也是机器能比我们做的更好的一件事.
封装相互依赖的异步方法
正如你看到的, 选择披萨的依赖有获取披萨菜单, 选择, 添加到购物车. 所以我们把这些依赖放在一个异步方法里, 饮料同理, 这也是为什么我们会有 selectPizza() 和 selectDrink() 两个异步方法.
并行执行
我们利用事件循环去非阻塞并行地执行这些异步方法, 通常会用的两个方法就是尽早的返回 Promise 和使用 Promise.all()
我们修复一下代码, 把这三个方法应用到我们的例子中去.
修改下代码
- async function selectPizza() {
- const pizzaData = await getPizzaData() // async call
- const chosenPizza = choosePizza() // sync call
- await addPizzaToCart(chosenPizza) // async call
- }
- async function selectDrink() {
- const drinkData = await getDrinkData() // async call
- const chosenDrink = chooseDrink() // sync call
- await addDrinkToCart(chosenDrink) // async call
- }
- (async () => {
- const pizzaPromise = selectPizza()
- const drinkPromise = selectDrink()
- await pizzaPromise
- await drinkPromise
- orderItems() // async call
- })()
- // Although I prefer it this way
- (async () => {
- Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call
- })()
我们把相互依赖的语句封装在各自的函数里, 现在同时去执行 selectPizza() 和 selectDrink()
第二个例子中, 我们需要处理未知数量的 Promise . 处理这种情况很简单, 我们先把 Promises 放进数组, 然后使用 Promise.all() 让他们并行执行, 之后等待他们全都执行完毕.
- async function orderItems() {
- const items = await getCartItems() // async call
- const noOfItems = items.length
- const promises = []
- for(var i = 0; i <noOfItems; i++) {
- const orderPromise = sendRequest(items[i]) // async call
- promises.push(orderPromise) // sync call
- }
- await Promise.all(promises) // async call
- }
- // Although I prefer it this way
- async function orderItems() {
- const items = await getCartItems() // async call
- const promises = items.map((item) => sendRequest(item))
- await Promise.all(promises) // async call
- }
希望本文可以引发你对 async/await 使用的思考, 也希望能帮助你提升程序的性能.
来源: https://juejin.im/entry/5af407c151882567336ac1f1