【譯】 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 中

相關文章
相關標籤/搜索