如何避免 await/async 地狱

img

原文地址:How to escape async/await hell
译文出自:夜色镇歌的我的博客javascript

async/await 把咱们从回调地狱中解救了出来,可是若是滥用就会掉进 async/await 地狱。java

本文中我会解释一下什么是 async/await 地狱,并会分享几个技巧去避免。编程

啥是 await/async 地狱

异步 Javascript 编程中,咱们一般会写许多 async 方法,而且使用 await 关键字去等待它,有不少时候下一行的执行并不依赖于上一行,可是咱们仍然使用了 await 去等待,因此可能会致使一些性能问题。数组

一个 await/async 地狱的例子

如何编写一个订购披萨和饮料的代码?它可能会像这样:promise

(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 包裹了起来,而后下面这些会依次执行。异步

  1. 获取披萨菜单
  2. 获取饮料菜单
  3. 从披萨菜单中选择披萨
  4. 从饮料菜单中选择饮料
  5. 把选好的披萨加到购物车
  6. 把选好的饮料加到购物车
  7. 下单

哪里错了?

正如我刚强调的,这些语句会依次执行,没有并发。仔细想一下,为啥我获取饮料菜单以前得先获取披萨菜单?这两份菜单我应该同时去获取。固然,选择披萨以前得先获取披萨菜单,这个规则一样适用于饮料。async

因此咱们能够得出结论,披萨相关的工做和饮料相关的工做能够并行进行,但涉及披萨相关工做的各个步骤须要按顺序进行(一步接着一步)。函数

另外一个糟糕的例子

这段代码会获取购物车中的购物项而且发出订购请求。性能

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 使用的思考,也但愿能帮助你提高程序的性能。

相关文章
相关标签/搜索