如何在 JS 循環中正確使用 async 與 await

譯者:前端小智javascript

medium.com/free-code-c…html


阿里雲最近在作活動,低至2折,有興趣能夠看看promotion.aliyun.com/ntms/yunpar…前端


爲了保證的可讀性,本文采用意譯而非直譯。 asyncawait 的使用方式相對簡單。 蛤當你嘗試在循環中使用await時,事情就會變得複雜一些。java

在本文中,分享一些在若是循環中使用await值得注意的問題。git

準備一個例子

對於這篇文章,假設你想從水果籃中獲取水果的數量。github

const fruitBasket = {
 apple: 27,
 grape: 0,
 pear: 14
};
複製代碼

你想從fruitBasket得到每一個水果的數量。 要獲取水果的數量,可使用getNumFruit函數。數組

const getNumFruit = fruit => {
  return fruitBasket[fruit];
};

const numApples = getNumFruit('apple');
console.log(numApples); //27
複製代碼

如今,假設fruitBasket是從服務器上獲取,這裏咱們使用 setTimeout 來模擬。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
複製代碼

最後,假設你想使用awaitgetNumFruit來獲取異步函數中每一個水果的數量。服務器

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')
}
複製代碼

在 for 循環中使用 await

首先定義一個存放水果的數組:app

const fruitsToGet = [「apple」, 「grape」, 「pear」];
複製代碼

循環遍歷這個數組:

const forLoop = async _ => {
  console.log('Start');
  
  for (let index = 0; index < fruitsToGet.length; index++) {
    // 獲得每一個水果的數量
  }

  console.log('End')
}
複製代碼

for循環中,過上使用getNumFruit來獲取每一個水果的數量,並將數量打印到控制檯。

因爲getNumFruit返回一個promise,咱們使用 await 來等待結果的返回並打印它。

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 返回處理結果。這意味着for循環中的await 應該按順序執行。

結果正如你所預料的那樣。

「Start」;
「Apple: 27」;
「Grape: 0」;
「Pear: 14」;
「End」;
複製代碼

這種行爲適用於大多數循環(好比whilefor-of循環)…

可是它不能處理須要回調的循環,如forEachmapfilterreduce。在接下來的幾節中,咱們將研究await 如何影響forEach、map和filter

在 forEach 循環中使用 await

首先,使用 forEach 對數組進行遍歷。

const forEach = _ => {
  console.log('start');

  fruitsToGet.forEach(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」;
複製代碼

但實際結果是不一樣的。在forEach循環中等待返回結果以前,JavaScrip先執行了 console.log('End')。

實際控制檯打印以下:

‘Start’
‘End’
‘27’
‘0’
‘14’
複製代碼

JavaScript 中的 forEach不支持 promise 感知,也不支持 asyncawait,因此不能在 forEach 使用 await

在 map 中使用 await

若是在map中使用await, map 始終返回promise數組,這是由於異步函數老是返回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 中使用 awaitmap 老是返回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')
}
複製代碼

運行結果以下:

若是你願意,能夠在promise 中處理返回值,解析後的將是返回的值。

const mapLoop = _ => {
  // ...
  const promises = fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit);
    return numFruit + 100
  })
  // ...
}
 
「Start」;
「[127, 100, 114]」;
「End」;
複製代碼

在 filter 循環中使用 await

當你使用filter時,但願篩選具備特定結果的數組。假設過濾數量大於20的數組。

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

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

  const moreThan20 =  fruitsToGet.filter(async fruit => {
    const numFruit = await 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 = fruitBasket[fruit]
    return numFruit > 20
  })
  
  console.log(moreThan20) 
  console.log('END')
}


// 打印結果
Start
["apple", "grape", "pear"]
END
複製代碼

爲何會發生這種狀況?

當在filter 回調中使用await時,回調老是一個promise。因爲promise 老是真的,數組中的全部項都經過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') }

在 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')
}
複製代碼

運行結果:

當你在 reduce 中使用await時,結果會變得很是混亂。

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

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

  console.log(sum)
  console.log('End')
}
複製代碼

[object Promise]14 是什麼 鬼??

剖析這一點頗有趣。

  1. 在第一次遍歷中,sum0numFruit27(經過getNumFruit(apple)的獲得的值),0 + 27 = 27

  2. 在第二次遍歷中,sum是一個promise。 (爲何?由於異步函數老是返回promises!)numFruit0.promise 沒法正常添加到對象,所以JavaScript將其轉換爲[object Promise]字符串。 [object Promise] + 0object Promise] 0

  3. 在第三次遍歷中,sum 也是一個promisenumFruit14. [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')
}
複製代碼

可是從上圖中看到的那樣,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最簡單(也是最有效)的方法是

  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') }

這個版本易於閱讀和理解,須要一秒鐘來計算水果總數。

從上面看出來什麼

  1. 若是你想連續執行await調用,請使用for循環(或任何沒有回調的循環)。

  2. 永遠不要和forEach一塊兒使用await,而是使用for循環(或任何沒有回調的循環)。

  3. 不要在 filterreduce 中使用 await,若是須要,先用 map 進一步驟處理,而後在使用 filterreduce 進行處理。

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

爲了回饋讀者,《大遷世界》不按期舉行(每月一到三次),現金抽獎活動,保底200,外加用戶讚揚,但願你能成爲大遷世界的小錦鯉,快來試試吧

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

[1]: github.com/qq449245884… [2]: /img/bVbtmPA [3]: /img/bVbtmPA [4]: /img/bVbtnQ0 [5]: /img/bVbtnTc [6]: /img/bVbtnXe [7]: /img/bVbtnZF [8]: /img/bVbtn0o [9]: /img/bVbtn0R [10]: /img/bVbtn0R
相關文章
相關標籤/搜索