【译】 js 循环中正确使用 async 与 await

原文连接:JavaScript async and await in loopsjavascript

前言

我在最近项目中遇到了批量申请的一个需求,当时只有单个申请的接口,因而我想到了循环数组请求接口的解决办法,因而就赶上了 async/await 和 循环的问题。我发如今 forEach 中使用 async/await 没有生效,因而在谷歌过程当中发现了问题所在,这篇文章讲解的十分详细,案例完整易于理解,是篇不可多得的好文章,因而翻译出来给你们参考,有什么问题你们能够在评论区一块儿探讨!java

噢?你问我最终怎么解决的? 后端同窗给了我一个批量申请的接口。git

正文

基础的 async 和 await 的使用相对简单,当你试图在循环中使用 await 时,事情就会变得有点复杂了。github

案例

举个例子,比方你想知道水果篮 fruitBasket 中的水果数量。后端

const fruitBasket = {
    apple: 27,
    grape: 0,
    pear: 14
}
复制代码

你想取得水果篮中每种水果的数量。为了获取它们,你能够定义一个 getNumFruit 函数。数组

const getNumFruit = fruit => {
    return fruitBasket[fruit]
}
const numApples = getNumFruit('apple')
console.log(numApples)	// 27
复制代码

如今,比方说 fruitBasket 位于远程服务器上。访问它须要花费一秒钟。咱们可使用 timeout 定时器来模拟这一秒的延迟。promise

const sleep = ms => {
    return new Promise(resolve => setTimeout(resolve, ms))
}
const getNumFruit = fruit => {
    return sleep(1000).then(v => fruitBasket[fruit])
}
getNumFruit('apple')
	.then(num => console.log(num)) //27
复制代码

假设你不想使用 Promise 操做异步任务了,你想使用 async / await 这回调终结者来用同步的方式去执行异步任务,以下:服务器

const control = async _ => {
    console.log('Start')
    
    const numApples = await getNumFruit('apple');
    console.log(numApples);
    
    const numGrapes = await getNumFruit('grape');
    console.log(numGrapes);
    
    const numPears = await getNumFruit('pear');
    console.log(numPears);
    
    console.log('End')
}
复制代码

img

在 for 循环中使用 Await

假设咱们定义一个水果数组。app

const fruitsToGet = ['apple', 'grape', 'pear']
复制代码

循环遍历这个数组异步

const forLoop = async _ => {
    console.log('Start')
    
    for(let index = 0; index < fruitsToGet.length; index++) {
        // Get num of each fruit
    }
    
    console.log('End')
}
复制代码

在这个 for 循环中,咱们将使用 getNumFruit 来获取并打印每种水果的数量。

由于 getNumFruit 返回一个 promise,咱们等待 resolved 结果的返回再打印。

const forLoop = async _ => {
    console.log('Start')
    
    for (let index = 0; index < fruitsToGet.length; index ++) {
        const fruit = fruitsToGet[index]
        const numFruit = await getNumFruit(fruit)
        console.log(numFruit)
    }
    
    console.log('End')
}
复制代码

当你使用 await,你可能指望 JavaScript 能够暂停执行直到等到 promise 返回结果。这意味着 await 在一个 for 循环中应该是按顺序执行的的

而结果正是你所指望的:

'Start'
'Apple: 27'
'Grape: 0'
'Pear: 14'
'End'
复制代码

img

这种行为在大部分循环中有效(像 while 和 for of循环)...

可是它不能处理须要回调的循环。好比 forEach、map、filter 和 reduce。在接下来几节中,咱们将研究 await 如何影响 forEach、map 和 filter。

在 forEach 循环中使用 await

仍是上面的示例,首先,先遍历水果数组。

const forEachLoop = _ => {
    console.log('Start')
    
    fruitsToGet.forEach(fruit => {
        // Send a promise for each fruit
    })
    
    console.log('End')
}
复制代码

而后咱们尝试使用 getNumFruit 来获取水果数量。(注意在回调函数中的 async 关键字,咱们须要这个 async 由于 await 在回调中)。

const forEachLoop = _ => {
    console.log('Start')
    
    fruitsToGet.forEach(async fruit => {
        const numFruit = await getNumFruit(fruit)
        console.log(numFruit)
    })
    
    console.log('End')
}
复制代码

你大概指望控制台这样打印:

'Start'
'27'
'0'
'14'
'End'
复制代码

但实际结果不是这样,JavaScript 在 forEach 循环中的 promise 得到结果以前调用了 console.log('End').

'Start'
'End'
'27'
'0'
'14'
复制代码

Console logs 'Start' and 'End' immediately. One second later, it logs 27, 0, and 14.

其实缘由很简单,那就是 forEach 只支持同步代码。

能够参考下 Polyfill 版本的 forEach,简化之后相似就是这样的伪代码。

while (index < arr.length) {
  callback(item, index)   //也就是咱们传入的回调函数
}
复制代码

从上述代码中咱们能够发现,forEach 只是简单的执行了下回调函数而已,并不会去处理异步的状况。 而且你在 callback 中即便使用 break 也并不能结束遍历。

为啥 for…of 内部就能让 await 生效呢。

由于 for…of 内部处理的机制和 forEach 不一样,forEach 是直接调用回调函数,for…of 是经过迭代器的方式去遍历。

在 map 中使用 await

若是你在 map 中使用 await,map 将老是返回一个 promise 数组。

const mapLoop = async _ => {
    console.log('Start')
    
    const numFruits = await fruitsToGet.map(async fruit => {
        const numFruit = await getNumFruit(fruit)
        return numFruit
    })
    
    console.log(numFruits)
    console.log('End')
}
复制代码
'Start'
'[Promise, Promise, Promise]'
'End'
复制代码

Console loggs 'Start', '[Promise, Promise, Promise]', and 'End' immediately

若是你在 map 中使用 await,map 老是返回 promises,你必须等待 promises 数组获得处理。 或者经过 await Promise.all(arrayOfPromises) 来完成此操做。

const mapLoop = async _ => {
    console.log('Start')
    
    const promises = fruitsToGet.map(async fruit => {
        const numFruit = await getNumFruit(fruit)
        return numFruit
    })
    
    const numFruits = await Promise.all(promises);
    console.log(numFruits);
    
    console.log('End')
}
复制代码

运行结果以下:

'Start'
'[27, 0, 14]'
'End'
复制代码

Console logs 'Start'. One second later, it logs '[27, 0, 14] and 'End'

若是你愿意,能够在promise 中处理返回值,解析后的将是返回的值。

const mapLoop = async _ => {
  // ...
  const promises = fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit)
    // Adds onn fruits before returning
    return numFruit + 100
  })
  // ...
}
复制代码
'Start'
'[127, 100, 114]'
'End'
复制代码

在 filter 循环中使用 await

当你使用 filter 时,但愿筛选具备特定结果的数组。假设过滤数量大于 20 的数组。

若是你正常使用 filter(没有 await),以下:

const filterLoop = _ => {
    console.log('Start')
    
    const moreThan20 = fruitsToGet.filter(fruit => {
        const numFruit = fruitBasket[fruit]
        return numFruit > 20
    })
    
    console.log(moreThan20)
    console.log('End')
}
复制代码
Start
["apple"]
END
复制代码

filter 中的 await 不会以相同的方式工做,实际上,它根本不起做用,你会获得未过滤的数组。

const filterLoop = async _ => {
    console.log('Start')
    
    const moreThan20 = await fruitsToGet.filter(async fruit => {
        const numFruit = await getNumFruit(fruit)
        return numFruit > 20
    })
    
    console.log(moreThan20)
    console.log('End')
}
复制代码
'Start'
['apple', 'grape', 'pear']
'End'
复制代码

这是为何呢?

当你在 filter 回调中使用 await 时,回调老是会返回一个 promise。由于 promises 老是真的,数组中的全部项都经过filter 。在filter 使用 await类如下这段代码

const filtered = array.filter(() => true)
复制代码

在filter使用 await 正确的三个步骤

  1. 使用map返回一个promise 数组
  2. 使用 await 等待处理结果
  3. 使用 filter 对返回的结果进行处理
const filterLoop = async _ => {
    console.log('Start')
    const promises = await fruitsToGet.map(fruit => getNumFruit(fruit))
    const numFruits = await Promise.all(promises)
    const moreThan20 = fruitsToGet.filter((fruit, index) => {
        const numFruit = numFruits[index]
        return numFruit > 20
    })
    
    console.log(moreThan20)
    console.log('End')
}
复制代码
Start
[ 'apple' ]
End
复制代码

Console shows 'Start'. One second later, console logs '['apple']' and 'End'

在 reduce 使用 await

若是想要计算 fruitBastet 中的水果总数。 一般可使用 reduce 循环遍历数组并将数字相加。

const reduceLoop = _ => {
  console.log('Start');

  const sum = fruitsToGet.reduce((sum, fruit) => {
    const numFruit = fruitBasket[fruit];
    return sum + numFruit;
  }, 0)

  console.log(sum)
  console.log('End')
}
复制代码

img

当你在 reduce 中使用await时,结果会变得很是混乱。

const reduceLoop = async _ => {
  console.log('Start')

  const sum = await fruitsToGet.reduce(async (sum, fruit) => {
    const numFruit = await getNumFruit(fruit)
    return sum + numFruit
  }, 0)

  console.log(sum)
  console.log('End')
}
复制代码
'Start'
'[object Promise]14'
'End'
复制代码

Console logs 'Start'. One second later, it logs '[object Promise]14' and 'End'

[object Promise]14 是什么 鬼??

剖析这一点颇有趣。

  1. 在第一次遍历中,sum为0。numFruit是27(经过getNumFruit(apple)的获得的值),0 + 27 = 27。
  2. 在第二次遍历中,sum是一个promise。 (为何?由于异步函数老是返回promises!)numFruit是0.promise 没法正常添加到对象,所以JavaScript将其转换为[object Promise]字符串。 [object Promise] + 0 是object Promise] 0。
  3. 在第三次遍历中,sum 也是一个promise。 numFruit是14. [object Promise] + 14是[object Promise] 14。

这意味着,你能够在reduce回调中使用await,可是你必须记住先等待累加器!

const reduceLoop = async _ => {
  console.log('Start');

  const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
    const sum = await promisedSum;
    const numFruit = await fruitBasket[fruit];
    return sum + numFruit;
  }, 0)

  console.log(sum)
  console.log('End')
}
复制代码

img

可是从上图中看到的那样,await 操做都须要很长时间。 发生这种状况是由于reduceLoop须要等待每次遍历完成promisedSum。

有一种方法能够加速reduce循环,若是你在等待promisedSum以前先等待getNumFruits(),那么reduceLoop只须要一秒钟便可完成:

const reduceLoop = async _ => {
  console.log('Start');

  const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
    const numFruit = await fruitBasket[fruit];
    const sum = await promisedSum;
    return sum + numFruit;
  }, 0)

  console.log(sum)
  console.log('End')
}
复制代码

img

这是由于reduce能够在等待循环的下一个迭代以前触发全部三个getNumFruit promise。然而,这个方法有点使人困惑,由于你必须注意等待的顺序。

在reduce中使用wait最简单(也是最有效)的方法是

  1. 使用map返回一个promise 数组
  2. 使用 await 等待处理结果
  3. 使用 reduce 对返回的结果进行处理
const reduceLoop = async _ => {
  console.log('Start')

  const promises = fruitsToGet.map(getNumFruit)
  const numFruits = await Promise.all(promises)
  const sum = numFruits.reduce((sum, fruit) => sum + fruit)

  console.log(sum)
  console.log('End')
}
复制代码

这个版本易于阅读和理解,须要一秒钟来计算水果总数。

img

从上面看出来什么

  1. 若是你想连续执行await调用,请使用没有回调的循环(for…of 、 for 循环、 while循环)
  2. 永远不要和 forEach 一块儿使用await
  3. 不要在 filter 和 reduce 中使用 await,若是须要,先用 map 进一步骤处理,而后在使用 filter 和 reduce 进行处理。

参考:为啥 await 不能用在 forEach 中

相关文章
相关标签/搜索