はじめに
forで書いたコードをレビューで「非同期の処理がないなら可読性向上のためにforEachの処理に変えたらどう?」とご指摘をいただいた時に新しく知ったことです。
修正したコード
// 修正前
for (let index = 0; index < docArr.length; index += 1) {
console.log(docArr[index]);
}
// 修正後
docArr.forEach((doc, index) => {
console.log(docArr[index]);
});
気付いたこと
ループ処理をスキップしたい時、forEachの中ではcontinue
の代わりにreturn
を書くこと
ちなみに以下のような処理の違いがあります。
return
は関数の中で使用され、関数の実行を終了します。
continue
はループ内で使用され、そのイテレーションを終了し、次のイテレーションに進みます。
forEachは同期的な処理をしているため、非同期処理を行うには不十分
以下のようにコールバックで非同期関数を呼び出ししましたが、逐次処理がされていません。
// 参考URLのコードを実行しました
const f = (value) => {
return new Promise((resolve) =>
setTimeout(() => {
console.log(value);
resolve();
}, Math.random() * 1000))
};
["A", "B", "C", "D", "E"].forEach(async (v) => {
await f(v);
});
console.log("終了");
// 結果
終了
C
D
E
B
A
for以外でループ内で非同期を実行したい場合のやり方
方法1:for...ofを使用する
const arr = ["A", "B", "C", "D", "E"];
let p = Promise.resolve();
for (const v of arr) {
p = p.then(() => f(v));
}
p.then(() => console.log("終了"));
もしくは
async function callArr() {
for (const v of arr) {
await f(v);
}
console.log("終了");
}
callArr();
方法2:forEachを工夫する
// 参考URLのコードを実行しました
let p = Promise.resolve();
["A", "B", "C", "D", "E"].forEach((v) => (p = p.then(() => f(v))));
p.then(() => console.log("終了"));
Promise.resolve()は解決済み(fulfilled)のPromiseを生成します。
pが最初に Promise.resolve() で初期化された後、then以降の() => f(v)の処理が走ります。p= で、新しいPromiseを再代入しています。そのため、前のPromiseが解決された後に、新しいPromiseが生成され、次の非同期処理が順次待機することになります。
方法3:reduceを使う
reduceメソッドは各配列の要素に対して、コールバック関数を適用し、最終的に一つの値にまとめます。
// 参考URLのコードを実行しました
["A", "B", "C", "D", "E"]
.reduce((p, v) => p.then(() => f(v)), Promise.resolve())
.then(() => console.log("終了"));
reduce(callbackFn, initialValue)の形をとっています。
callbackFnは (p, v) => p.then(() => f(v))
initialValueは Promise.resolve()
となります。
pは前回の callbackFn の呼び出し結果の値を受け取ります。
初回の呼び出しでは initialValue がある場合はinitialValueが入ります。
つまり、初回ではpにinitialValueのPromise.resolve()が入り、2回目以降はf(v)の結果のPromiseが渡されることとなります。
vは現在の要素の値です。初回の呼び出しでは initialValue が指定された場合は array[0] の値です。
ダメだった方法:Promise.allとmapを使う
// 参考URLのコードを実行しました
const main = async () => {
await Promise.all(["A", "B", "C", "D", "E"].map((v) => f(v)));
console.log("終了");
};
main();
// 結果
B
D
A
C
E
終了
Promise.allでPromiseがすべて解決されるまで待つので、順番に出力されるかと思いました。しかし、各Promiseの解決順序を保証しないので、このような結果になりました。
おわりに
ループ内で非同期処理を行うならforかfor...ofがよくて、非同期処理を使用しない場合はforよりもforEachの方が記載がシンプルでいいなと思いました。reduceについて詳しく知らなかったので勉強になりました。
参考
https://zenn.dev/sora_kumo/articles/612ca66c68ff52
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce