##処理方法とその特徴
Array.prototype.forEach()のコールバック関数に非同期の関数であるデータベースクエリなどを入れてループ処理を行った際の挙動が気になったため、いくつかのパターン別にサンプルコードを作成して確認してみました。今回は、Part1として、forEach + async/awaitから見ていきます。
その特徴ですが、非同期関数のループ処理においてforEach + async/awaitを使用すると、awaitが効かず、forEachが爆走します。
##ループ処理の内容
フェイク非同期関数(fakeDatabaseQuery)を作成し、forEachのコールバックとしてフェイク非同期関数を100回実行するとどのような挙動になるのか、試してみました。
-
関数processArrayの中にあるforEachのループ処理によって、関数fakeDatabaseQuery()が連続して実行されますが、forEachの場合はasync/awaitによるサスペンドが効かないため、promiseオブジェクトが連続的に作成されるものの、それぞれのpromiseはresolveされた順番で次のプロセスに渡されます。
-
関数fakeDatabaseQuery()の中でpromiseオブジェクトを生成しますが、setTimeoutは、ランダムなelapseの値を使って、resolveする際の遅延を作り出しています。そのため、連続してpromiseが生成されるのですが、elapseの値がランダムであるため、resolveされる順番がバラバラとなります。
-
awaitは常にpromiseオブジェクトを待ち受ける役割となります。今回は、awaitによるfakeDatabaseQuery()のサスペンドとレジュームは効きませんが、promiseがresolveされ次第(=>この状態をfulfilledといいます)、awaitはpromiseから値を取り出して、変数objにその値を代入します。
-
awaitによってpromiseオブジェクトから取り出された値は、次のプロセスとして、fakeSearchResult.push()に渡され、その渡された順番でアレイfakeSearchResultに詰め込まれていきます。
-
ただし、その後ソートしているので、最終的には、当初のループ処理の順番とおりになっています。
##まとめ
プログラムを見ると、一見、async/awaitによりarrayのアイテムをひとつずつ順番に処理を行うように見えますが、実際にはそうはなりません。async/awaitによるサスペンドが全く効かず、暴走しているように感じますが、処理は速いです。フェイク非同期関数を実行する際、サスペンドなしで、イベントループにひたすら順番に投入していくイメージかと思います。
ちなみに、MDNのドキュメント(Array.prototype.forEach())には、
Note: forEach expects a synchronous function.
forEach does not wait for promises. Make sure you are aware of the implications while using promises (or async functions) as forEach callback.
と書いてあるのですが、たしかに、その通りになりました。
##その他
下記のサンプルコードは、Node.jsv14.15.5でテストしたものです。
また、私の環境(Ubuntu + Node.js)で実行したところ、処理時間は120ms~125ms程度でした。さらに、蛇足ですが、arrayのアイテム数を100=>10000(百倍)などにしても、処理時間はそれほど変わらず、120ms=>190msでした。
##サンプルコード
console.time();
let fakeSearchResult = [];
//forLoopを使って、整数1~100が入っているarrayを作ります
let arrayN = [];
const arrayLength = 100;
for (let i = 0; i < arrayLength; i++) {
arrayN[i] = i + 1;
}
//最後にソートをかけて、元の順番に戻すための関数
function arraySorter(array){
array.sort(function(a, b) {
return a.index - b.index;
});
}
//データベース検索を偽装したフェイク非同期関数
function fakeDatabaseQuery(item) {
//ランダム関数で2桁の整数を作成します
const elapse = Math.floor(Math.random() * 100);
//プロミスオブジェクトをリターンします
return new Promise(function(resolve, reject) {
const obj = {index:item, elapse:elapse}
//setTimeoutを使い、意図的に遅延を作りだしています
setTimeout(() => resolve(obj), elapse);
});
}
//forEachを使って、フェイク非同期関数をループ実行
function processArray(array) {
array.forEach(async (item) => {
//awaitはプロミスオブジェクトを待っていて、fulfilledになると、
//プロミスオブジェクトから値を取り出します
const obj = await fakeDatabaseQuery(item);
fakeSearchResult.push(obj);
if (fakeSearchResult.length === array.length){
console.log('fakeSearchResult-1:',fakeSearchResult);//順番がぐだぐだです
arraySorter(fakeSearchResult);//arrayにソートをかけて、当初の順番に戻します
console.log('fakeSearchResult-2:',fakeSearchResult);//ソートして元の順番に戻りました
console.timeEnd();
}
});
}
processArray(arrayN);