async/awaitとreduceの使い方

詰まったのでメモ。

同期

以下のように書くと

[1, 2, 3].reduce((acc, v) => acc + v, 0);

//6

「1,2,3という配列の要素を1つずつ順番に足していく」という処理になります。

これは超基本的なので、すんなり理解できる😀

非同期

以下のように書くと

(async () => {
  const sleep = (n) => new Promise((resolve) => setTimeout(resolve, n));

  [1, 2, 3].reduce(async (acc, v) => {
    await sleep(1000);
    console.log(v);
  }, 0);
})();

//出力結果
//1
//2
//3

「1,2,3という配列の要素を1秒のインターバルを挟んで順番に表示していく」という処理になるはずなので、順番に1,2,3と1秒ずつ間隔をあけて表示されていくと思いきや、実際は1,2,3が同時に表示されます。(ブラウザで実行してみてください)

一体なぜ・・・?🤔

こうなる理由は、同期処理だとreduceは以下のような動きになるけど

  • 1周目:すぐにスタートする
  • 2周目:1周目の終了を待ってからスタートする
  • 3周目:2周目の終了を待ってからスタートする

 

非同期処理にすると

  • 1周目:すぐにスタートする
  • 2周目:1周目の終了を待たずにスタートする
  • 3周目:2周目の終了を待たずにスタートする

という動きになってしまうため。

 

これを阻止するためには、以下のように処理の最初にawait accを追加します😎

(async () => {
  const sleep = (n) => new Promise((resolve) => setTimeout(resolve, n));

  [1, 2, 3].reduce(async (acc, v) => {
    await acc;
    await sleep(1000);
    console.log(v);
  }, 0);
})();

//出力結果 
//1 
//2 
//3

これを追加することで

  • 1周目:
    →すぐにスタートする
  • 2周目:
    →1周目の終了を待たずにスタートするが、最初にawait accと書かれているので、1周目の結果が返ってくるまでストップする
  • 3周目:
    →2周目の終了を待たずにスタートするが、最初にawait accと書かれているので、2周目の結果が返ってくるまでストップする

という動きになります。なので順番に1,2,3と1秒ずつ間隔をあけて表示されていきます。

 

このように「1周目の終了を待たずに2周目がスタートする」という処理になるのは、reduceに限らず

  • foreach
  • filter
  • map

なども同じ。

 

逆に

  • for
  • for of
  • for in

などはasyncを使っても「1周目の終了を待ってから2周目をスタートする」という感じで同期的に実行できるっぽい。

足し算

最後に、非同期reduceを使った足し算バージョンを書いて終わりにします。

(async () => {
  const sleep = (n) => new Promise((resolve) => setTimeout(resolve, n));

  const add = await [1, 2, 3].reduce(async (acc, v) => {
    await acc;
    console.log(v);
    await sleep(1000);
    return (await acc) + v;
  }, 0);

  console.log(add);
})();

//実行結果
//1
//2
//3
//6

このように書くと、順番に1,2,3と1秒ずつ間隔をあけて表示されていき、最後に6と表示されます。

  • return (await acc) + v;の部分は、return acc + v;と書いてはダメ。
  • awaitは「渡されたPromiseが解決されるまで待ち、解決後に値を返却する」ものなので、今回の場合でいうと元々のaccを上書きはしない。
  • なので、「最初にawait acc;と書いているので、accはすでに解決済みでしょ?」と思いきや、その返却値はどこにも利用されずに消えているので、その後にreturn acc + v;と書いたとしても、その中のaccは未解決のaccのままになる。なのでreturn文でもawaitが必要。
    (もしくは最初のawait acc;の返り値を利用する)

 

おわり

コメント

タイトルとURLをコピーしました