【JavaScript】 forEachでawaitが使えないんですけど?
更新日:2021/05/28
forEach内でawaitを使用して非同期処理の終了を待つコードを作成したら、想定した動作になりませんでした。
どうしてなのか、その理由と解決方法を探っていきます。
まずはforEachを使用しない例
まずはforEachしないで、awaitを使用する例です。
次のデモスタートボタンを押してみてください。
1秒経過を待ってメッセージ出力。
その後2秒経過を待ってメッセージ出力と、時間の経過を待って次の処理を行っています。
次は、そのコード。
HTML
<button id="bt1">デモスタート</button>
<div id="output1"></div>
JavaScript
window.addEventListener( "DOMContentLoaded" , ()=> {
const outputId = document.getElementById("output1");
const output = t => outputId.innerHTML += t;
const timer = sec =>{
return new Promise( resolve => {
output(`<p>${sec}秒経過待ち</p>` );
setTimeout( ()=>{
resolve();
},sec * 1000);
});
};
const func = async ()=>{
const seclist = [1,2,3];
for( let i = 0; i < seclist.length ; i ++ ) {
await timer( seclist[i] );
output( `<p>${seclist[i]}秒経過しました</p>` );
}
};
document.getElementById("bt1").addEventListener("click",func)
});
ボタンが押されて関数funcが呼び出されます。
関数funcは、forループを使って、timer関数を呼び出しています。
この時、timer関数でセットしたタイマーが終了するのを待ってから、次のループを処理しています。
ポイントは、funcがasync関数である点と、async関数内で処理待ちをしたい関数呼び出しにawaitを付加している点です。
詳しくは、次のページを読んでみてください。
■【JavaScript】 async/awaitを解説します
forEachを使ってみる
配列をforループで回すなんてダサい!
そんな声に答えて、forEachを使った方法に書き換えてみます。
forEachにかえてみた
const func = async ()=>{
const seclist = [1,2,3];
seclist.forEach( sec =>{ await timer(sec); } );
};
これを実行すると、次のエラーがでます。
Firefox : Uncaught SyntaxError: await is only valid in async functions and async generators
Chrome : Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
つまり、「awaitはasync関数内でしか使えませんよ」と怒られてしまったのです。
でもちょっと待って。
func関数にasyncついてるから、ちゃんとasync関数内でawait使ってますよ?
どうしてエラーって言われてしまうのでしょうか?
冷静になって考えると、forEachの引数が関数だったりします。
sec =>{ await timer(sec); } ←関数
この関数はasync関数ではないので、awaitは使えないということなんですね。
ということで、secの前にasyncをつければ解決です。
asyncをつけてみた
const func = async ()=>{
const seclist = [1,2,3];
seclist.forEach( async sec =>{ await timer(sec); } );
};
早速実行してみましょう。
なんか違う!
最初のデモと動作が同じではありません。
またまた、どうしてでしょうか?
【JavaScript】 forEach/map/filter/reduceを根本的に理解するで、forEachの内部処理の要点をお伝えしています。
そこで紹介しているコードを、簡略化したのが次のコードです。
Array.prototype.forEach = function( callBack ){
const length = this.length;
for( let i = 0 ; i < length ; i ++ ){
callBack( this[ i ] , i , this ); // コールバック呼び出し
}
};
つまり、forEachは引数で受け取ったコールバック関数を、単純に配列の要素に対して呼び出しているだけということです。
コールバック関数の結果を考慮しませんし、ましてや非同期の解決待ちなんてしていません。
そのため、今回のタイマー待ちのコードは、引数1,2,3で3回timer関数を呼び出して終了です。
その後、timer関数内でセットしたタイマーが各コールバック関数を呼び出しています。
コールバック関数内でawaitは使用してはいけない
関数やメソッドにコールバック関数を渡す場合、async関数を渡すべきではありません。
async関数の実行結果はPromiseオブジェクトですが、多くのメソッドはこの実行結果を想定していないため、何らかの不具合がおこる可能性があるからです。
awaitで非同期の解決を待つ場合は、async関数内で使用しているかよく確認しましょう。
なお、次のように for-of を使用すると、forループでもカウンターを用意しなくて済みます。
おススメです。
const func = async ()=>{
const seclist = [1,2,3];
for( const sec of seclist){
await timer(sec);
output( `<p>${sec}秒経過しました</p>` );
}
};
for-await-ofの利用
JavaScriptには for-await-of という構文があります。
Promiseの配列を上から順番に結果待ちしてくれる構文です。
for-await-ofについては、こちらをご覧ください。
■【JavaScript】 for-await-of構文を理解してみる
これまでのコードを、for-await-ofに書き換えてみます。
for-await-ofに書き換えてみる
const timer = sec =>{
return new Promise( resolve => {
output(`<p>${sec}秒経過待ち</p>` );
setTimeout( ()=>{
resolve(sec);
},sec * 1000);
});
};
const func = async ()=>{
const seclist = [1,2,3];
const timers = seclist.map( sec =>timer( sec ) );
for await ( const sec of timers ){
output( `<p>${sec}秒経過しました</p>` );
}
};
関数funcで始めに、mapメソッドでtimer関数の結果を配列にしています。
その後、for-await-ofで、順番に結果待ちをしています。
なお、変数secにはtimer関数の結果がセットされます。
つまり、resolve(sec)のsecの値が、代入されます。
実行してみましょう。
失敗パターンでした。
mapでtimer関数を呼び出した段階で、タイマーがスタートしているので、こんな結果になります。
そこで、for-await-ofループでプロミスを取り出す時に、タイマーをスタートするように変更してみます。
ジェネレーター使ってみる
const timer = function*(seclist) {
for( const sec of seclist ){
yield new Promise( resolve => {
output(`<p>${sec}秒経過待ち</p>` );
setTimeout( ()=>{
resolve(sec);
},sec * 1000);
});
}
};
const func = async ()=>{
const seclist = [1,2,3];
const timers = timer( seclist );
for await ( const sec of timers){
output( `<p>${sec}秒経過しました</p>` );
}
};
timer関数をジェネレーター関数に変更してみました。
これで、for-await-ofがtimersにアクセスするたびに、タイマーがスタートするようになります。
ジェネレーター関数って何?という方は次のページを読んでみてください。
■【JavaScript】 Generator(ジェネレーター)とyieldってなんだ?関数とは何が違うのか
実行してみましょう。
うまくいきました。
更新日:2021/05/28
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。