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

2021-02-19 前端桃園

async 與 await 的使用方式相對簡單。 當你嘗試在循環中使用await時,事情就會變得複雜一些。

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

準備一個例子

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

const fruitBasket = {
 apple: 27,
 grape: 0,
 pear: 14
};

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

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

const numApples = getNumFruit('apple');
console.log(numApples); 

現在,假設fruitBasket是從伺服器上獲取,這裡我們使用 setTimeout 來模擬。

const sleep = ms => {
  return new Promise(resolve => setTimeout(resolve, ms))
};

const getNumFruie = fruit => {
  return sleep(1000).then(v => fruitBasket[fruit]);
};

getNumFruit("apple").then(num => console.log(num)); 

最後,假設你想使用await和getNumFruit來獲取異步函數中每個水果的數量。

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

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

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」;

這種行為適用於大多數循環(比如while和for-of循環)…

但是它不能處理需要回調的循環,如forEach、map、filter和reduce。在接下來的幾節中,我們將研究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 感知,也不支持 async 和await,所以不能在 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 中使用 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')
}

運行結果如下:

如果你願意,可以在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 正確的三個步驟

使用map返回一個promise 數組

使用 await 等待處理結果

使用 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 是什麼 鬼??

剖析這一點很有趣。

在第一次遍歷中,sum為0。numFruit是27(通過getNumFruit(apple)的得到的值),0 + 27 = 27。

在第二次遍歷中,sum是一個promise。 (為什麼?因為異步函數總是返回promises!)numFruit是0.promise 無法正常添加到對象,因此JavaScript將其轉換為[object Promise]字符串。 [object Promise] + 0 是object Promise] 0。

在第三次遍歷中,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')
}

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

使用map返回一個promise 數組

使用 await 等待處理結果

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

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

從上面看出來什麼

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

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

不要在 filter 和 reduce 中使用 await,如果需要,先用 map 進一步驟處理,然後在使用 filter 和 reduce進行處理。

文章來自 sf 的小智,有興趣可以關注他的公眾號「大遷世界」
原文連結:https://segmentfault.com/a/1190000019357943

相關焦點

  • 如何正確合理使用 JavaScript async/await
    它提供了使用同步樣式代碼異步訪問 resoruces的方式,而不會阻塞主線程。然而,它們也存在一些坑及問題。在本文中,將從不同的角度探討 async/await,並演示如何正確有效地使用這對兄弟。async 作用是什麼從 MDN 可以看出:async 函數返回的是一個 Promise 對象。
  • Async/Await有什麼用?
    要知道為什麼總是返回 Promise,就需要了解異步函數的第二種超級功能:使用 await 語句實現 promise 的能力。在異步函數中編寫代碼時,可以使用 await 語句。該語句在異步函數外部不可用。
  • Python async/await教程
    例如,使用Python異步協同程序,在繼續執行前不需要等待HTTP請求完成,你可以提交請求,一邊做其他隊列中等待的工作一邊等待HTTP請求完成。這可能需要思考更多來獲得正確的邏輯,但是你可以以更少的資源處理更多的工作。即便如此,異步函數的語法和執行在Python這樣的語言中實際上並不困難。異步性似乎是node.js在伺服器端編程如此受歡迎的最大原因。
  • 如何用實例掌握Async/Await
    今天讓我們一起來探討如何用實例掌握Async/Await目錄1、簡介(callbacks, promises, async/await)2、實例—貨幣轉換器從2個API’s接收異步數據。Async函數是通過在函數聲明之前加上Async來創建的,如下所示:異步函數可以用await暫停,await是只能在異步函數中使用的關鍵字。await返回異步函數完成後返回的任何內容。
  • 現場教學,優雅地處理基於 Vue CLI 項目中的 async await 異常
    前言了解過在實際項目中處理 async await 異常方式同學應該知道,常見的捕獲方式:使用 errorCaptured 來調用 async 函數。使用 plugin 或 loader 在打包的時候統一包裹 try catch。
  • async/await,了解一下?
    為什麼是async/await在 es6 中,我們可以使用 Generator 函數控制流程,如下面這段代碼:function* foo(x) {    yield x + 1;    yield x + 2;    return x + 3;}我們可以根據不斷地調用 Generator
  • 理解JavaScript 的 async/await
    另外還有一個很有意思的語法規定,await 只能出現在 async 函數中。然後細心的朋友會產生一個疑問,如果 await 只能出現在 async 函數中,那這個 async 函數應該怎麼調用?c:\var\test> node --harmony_async_await .Promise { 'hello async' }所以,async 函數返回的是一個 Promise 對象。從文檔中也可以得到這個信息。
  • 反正我是這樣處理async...await 錯誤的,你呢?
    /await,貌似很多人都是沒有異常處理的習慣,說的好像接口對接完100%就麼問題了似的(直接把鍋給後端,簡直就是機智如我)常規的開發中這樣的:如下情況,也個頁面顯示一個app模塊和一個熱點新聞模塊,它們之間其實是沒有依賴關係的如果獲取appp的接口getApps出現掛了的情況,那getNews
  • 深入async/await知多少
    async/await如果每一步都要自己的封裝那這個功能使用門檻就非常高了,為也讓這功能更好地使用所以.net提供了一個async/await的基礎實現,那就是Task.Task提供一系列完善的功能主要包括:自有的awaiter線程調度器,wait同步等待行主和TaskCompletionSource<T>等一系列簡化async/await
  • 代碼詳解:Async/Await優於基礎Promises的7大原因
    函數前有關鍵詞async。關鍵詞await 只能用於async定義的函數之內。任一async函數都能隱式返回promise,其解析值是從該函數返回的任意值(在此指字符串「done」)。2. 上一點意味著不能在代碼頂部使用await,因為它不在async定義的函數範圍之中。
  • 壓箱底筆記:Promise和Async/await的理解和使用
    如何先改狀態再指定回調?在執行器中直接調用 resolve()/reject()什麼時候才能得到數據?async function 用來定義一個返回 AsyncFunction 對象的異步函數。異步函數是指通過事件循環異步執行的函數,它會通過一個隱式的 Promise 返回其結果,。如果你在代碼中使用了異步函數,就會發現它的語法和結構會更像是標準的同步函數。MDN async_functionawait  操作符用於等待一個Promise 對象。
  • C# 中的Async 和 Await 的用法詳解
    在本文中,我們將共同探討並介紹什麼是Async 和 Await,以及如何在C#中使用Async 和 Await。同樣本文的內容也大多是翻譯的,只不過加上了自己的理解進行了相關知識點的補充,如果你認為自己的英文水平還不錯,大可直接跳轉到文章末尾查看原文連結進行閱讀。
  • JavaScript中的async/await的用法和理解
    昨天更新的是「JavaScript中的Promise使用詳解」,其實也就是說了下基本用法和自己對Promise
  • javascript解決異步async、await和co庫的實現
    相信大家都聽說過js中的回調地獄給代碼維護帶來了很大的阻礙,應用而生的也給出了N解決方案,從最初的promise,到co庫,再到es規範提供的api async、await等!接下來咱們聊的話題就是async和co庫的具體實現在學習前咱們了解幾個小知識點吧!
  • 實現一個 async/await (typescript 版)
    這次我們來實現一個 typescript 版本的 async/await。關於 async/await 的原理的文章,網上也有很多了,但是本文既然是使用 typescript 來寫,我們的 async/await 也是要能夠通過用戶傳入的函數自動推斷出結果,所以如何對其編寫 typescript 定義也是本文的一個重要板塊。
  • 理解C#中的 async await
    .NET reflector (也可使用 dnSpy 等) 反編譯一下程序集,然後一步一步來探究 async await 內部的奧秘。原來,雖然我們寫代碼時為了在 Main 方法中方便異步等待,將 void Main 改寫成了async Task Main,但是實際上程序入口仍是我們熟悉的那個 void Main。另外,我們可以看到異步 Main 方法被標註了AsyncStateMachine特性,這是因為在我們的原始碼中,該方法帶有修飾符async,表示該方法是一個異步方法。
  • async/await 原理及執行順序分析
    基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰,而且還支持 try-catch 來捕獲異常,非常符合人的線性思維。所以,要研究一下如何實現 async/await。
  • 8 張圖幫你一步步看清 async/await 和 promise 的執行順序
    說實話,關於js的異步執行順序,宏任務、微任務這些,或者async/await這些慨念已經有非常多的文章寫了。但是怎麼說呢,簡單來說,業務中很少用async,不太懂async呢,研究了一天,感覺懂了,所手癢想寫一篇 ,哈哈畢竟自己學會的知識,如果連表達清楚都做不到,怎麼能指望自己用好它呢?
  • 你必須了解的JavaScript關鍵字async和await
    注意,await只能在用async關鍵字標記的函數中使用。它的工作方式與Generator類似,在Promise完成之前暫停上下文中的執行。如果等待的表達不是Promise,那麼它就變成了Promise。
  • 在 Node.js 7 中甩掉 Callback Hell
    在幾個月之前,V8 引擎就實現了對 async/await 關鍵字的支持,Node.js 7中的 V8 經過幾次更新,終於在上一個 night build 版本中加入對async/await 的支持。異步編程的最高境界,就是根本不用關心它是不是異步, 所以 async/await 一直被譽為的 「殺手級解決方案」,讓你從回調地獄中解脫出來;現在就可以在 Node.js 7 中使用該關鍵字了,步驟如下:安裝 Node.js 7,可以使用 nvm 安裝;使用 async/await 寫一個簡單的示例使用 node --harmony-async-await