阅读 6249

[译文]如何避开 async/await 地狱

原文地址 How to escape async/await hell

async/await把我们从回调地狱中解放了出来,但是,人们也对其颇有微词.因为随之而来导致了async/await地狱的诞生.

在这篇文章,我会试图解释什么是async/await地狱,另外我也会分享一些避开它们的方法.

什么是 async/await 地狱?

当我们在编写JavaScript异步代码的时候,人们经常在一个接着一个的函数调用前面添加await关键字.这会导致性能问题,因为在通常情况下,一个语句的执行并不依赖前一个语句的执行,但是因为添加了await关键字,你仍旧需要等待前一个语句执行完才能执行一个语句.

一个 async/await 地狱的例子.

假设你写一段代码用来购买披萨和饮料,这段代码如下所示.

(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
})()
复制代码

从表面上看,这段代码语法是正确的,并且能够运行.但是,这并不是一个好的实现,因为它剔除了并发执行.接下来让我们了解一下这段代码是做什么的,这样我们更加明确其中的问题所在.

解释

我们把这段代码包裹在了一个异步的立即执行函数里面.下面的事情会按次序发生:

  1. 获得披萨的列表.
  2. 获得饮料的列表.
  3. 从披萨列表中选择披萨.
  4. 从饮料列表中选择饮料.
  5. 把选择的披萨加入购物车
  6. 把选择的饮料加入购物车.
  7. 确认订单

错误在哪里?

就像我在前面提到的那样,所有的语句都是一行接着一行执行的,这里不存在并发执行的情况.让我们仔细想想,为什么我们在获取饮料列表之前需要等待披萨列表的返回?我们应该尝试同时获取饮料和披萨的列表.然而,当我们需要选择披萨的时候,我们需要先获取披萨的列表.饮料也是如此.

因此我们确定,披萨相关的工作和饮料相关的工作能够同时执行,但是披萨相关的每一步工作需要按次序执行.(顺序执行)

另外一个坏例子

这段JavaScript代码会获得购物车里面的物品,然后发送确认订单的请求.

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关键字,这时函数开始执行了,这意味着await并不是函数执行的必要条件.这个异步函数会返回一个promise,这个promise我们可以在之后使用.

(async () => {
  const value = doSomeAsyncTask()
  console.log(value) // an unresolved promise
})()

复制代码

结果就是,编译器不知道你需要等待这个函数执行完成,因此编译器会在这个异步任务还没有完成的时候退出这个程序.因此我们需要await关键字.

promise有一个有趣的性质是你可以在前面的代码得到这个promise, 然后在后面的代码中等待这个promise完成.这是从async/await地狱中解脱的关键.

(async () => {
  const promise = doSomeAsyncTask()
  const value = await promise
  console.log(value) // the actual value
})()
复制代码

正如你所见到的那样,doSomeAsyncTask()返回了一个promise.这个时候,doSomeAsyncTask()已经开始执行了.为了得到这个promise的结果值,我们可以在这个promise前面添加await,JavaScript将会立刻停在这里不再执行下一行代码,直到获得了这个promise的返回值,再执行下一行代码.

如何逃离 async/await 地狱?

你应该跟随以下步骤来逃离async/await地狱.

找出所有以来其他语句执行的语句

在我们第一个例子里面,我们在选择披萨和饮料.因而我们得出结论,在选择披萨之前,我们需要获得披萨的列表.同时,在把披萨加入到购物车之前,我们需要选择披萨.可以认为这三个步骤是互相依赖的,我们不能在前一个步骤完成之前执行下一个任务. 但是,如果我们拓宽一下眼界,就会发现选择披萨并不会依赖于选择饮料,我们可以同时选择他们.这就是机器能做的比我们更好的地方. 至此,我们已经发现了一些语句依赖于其他的语句执行,但是另外一些语句不依赖.

把相互依赖执行的语句整合在异步函数里面.

正如我们所看到的,选择披萨需要几个互相依赖的语句,如获得披萨列表,选择其中一个披萨然后添加到购物车中.我们应该把这些语句整合在一个异步函数里面.这样我们将会得到两个异步函数,selectPizza()selectDrink()

并发的执行这些异步函数.

我们将利用event loop的优势来并发执行这些非阻塞异步函数.为了达成这个目标,我们常用的方法是先返回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
})()

// 我更喜欢下面这种实现.

(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems)   // async call
})()

复制代码

现在我们已经把这些语句整合到两个函数中,在每一个函数里面,每一个语句的执行依赖于前一个函数的执行.然后我们并发的执行selectPizza()selectDrink().

在第二个例子里面,我们需要解决未知数量的promise.解决这种情况非常简单:我们只需要创建一个数组然后把promise存入其中.然后使用Promise.all()方法,就能够并发的等待所有的promise返回结果.

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
}

// 我更喜欢下面这种实现 

async function orderItems() {
  const items = await getCartItems()    // async call
  const promises = items.map((item) => sendRequest(item))
  await Promise.all(promises)    // async call
}
复制代码

我喜欢这篇文章能够帮助你脱离async/await基础使用者的行列,同时能够帮助你提高你的程序性能. 如果你喜欢这篇文章,希望能够点赞并收藏.

关注下面的标签,发现更多相似文章
评论