原文連接: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')
}
複製代碼
這個版本易於閱讀和理解,須要一秒鐘來計算水果總數。