詰まったのでメモ。
同期
以下のように書くと
[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;
の返り値を利用する)
おわり
コメント