原文连接: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')
}
复制代码
假设咱们定义一个水果数组。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'
复制代码
这种行为在大部分循环中有效(像 while 和 for of循环)...
可是它不能处理须要回调的循环。好比 forEach、map、filter 和 reduce。在接下来几节中,咱们将研究 await 如何影响 forEach、map 和 filter。
仍是上面的示例,首先,先遍历水果数组。
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'
复制代码
其实缘由很简单,那就是 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 将老是返回一个 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'
复制代码
若是你在 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'
复制代码
若是你愿意,能够在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 时,但愿筛选具备特定结果的数组。假设过滤数量大于 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 正确的三个步骤
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
复制代码
若是想要计算 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')
}
复制代码
当你在 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'
复制代码
[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')
}
复制代码
可是从上图中看到的那样,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')
}
复制代码
这是由于reduce能够在等待循环的下一个迭代以前触发全部三个getNumFruit promise。然而,这个方法有点使人困惑,由于你必须注意等待的顺序。
在reduce中使用wait最简单(也是最有效)的方法是
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')
}
复制代码
这个版本易于阅读和理解,须要一秒钟来计算水果总数。