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
- })()
复制代码
从表面上看, 这段代码语法是正确的, 并且能够运行. 但是, 这并不是一个好的实现, 因为它剔除了并发执行. 接下来让我们了解一下这段代码是做什么的, 这样我们更加明确其中的问题所在.
解释
我们把这段代码包裹在了一个异步的立即执行函数里面. 下面的事情会按次序发生:
获得披萨的列表.
获得饮料的列表.
从披萨列表中选择披萨.
从饮料列表中选择饮料.
把选择的披萨加入购物车
把选择的饮料加入购物车.
确认订单
错误在哪里?
就像我在前面提到的那样, 所有的语句都是一行接着一行执行的, 这里不存在并发执行的情况. 让我们仔细想想, 为什么我们在获取饮料列表之前需要等待披萨列表的返回? 我们应该尝试同时获取饮料和披萨的列表. 然而, 当我们需要选择披萨的时候, 我们需要先获取披萨的列表. 饮料也是如此.
因此我们确定, 披萨相关的工作和饮料相关的工作能够同时执行, 但是披萨相关的每一步工作需要按次序执行.(顺序执行)
另外一个坏例子
这段 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
- }
复制代码
来源: https://juejin.im/post/5b9db6925188255c3b7d78cb