【JavaScript】 for-await-of構文を理解してみる
更新日:2021/01/11
ツイート
JavaScriptにはfor-await-ofという構文があります。
あまり見慣れない構文なので、調べてみました。
for-ofとfor-await-ofの違い
for-ofはイテレーターからデータを受け取って、順番に処理する構文です。
for( let 変数 of オブジェクト ){ 変数を使用した処理... }
for-await-ofも基本的には同じ動作をしますが、受け取ったデータがPromiseオブジェクトの場合は、履行されるのを待ち、結果をループの値として返します。
for await ( let 変数 of オブジェクト ){ 変数を使用した処理... }
通常の値を返すイテレータの場合
次のコードはfor-ofの簡単な例です。
const obj ={ data1:"A", data2:"B", data3:"C", // イテレータをジェネレーターで実装 *[Symbol.iterator](){ // [Symbol.iterator] : function* () の短縮表現 yield this.data1; yield this.data2; yield this.data3; } }; for( let data of obj){ console.log( data ); // 結果: A // B // C }
上の例はobj内にイテレータを組み込み、3つのプロパティdata1、data2、data3を順番に取り出せるようにしています。
そしてfor-of構文で、イテレータから返された値を順番に受け取り、ログ表示しています。
次に、for-ofの部分をfor-await-ofに変更してみます。
async function func(){ for await ( let data of obj){ console.log( data ); // 結果: A // B // C } } func();
for-await-ofは、「async関数の中でのみ使用可能」という決まりがあるので、関数化してあります。
結果はfor-ofと同じです。
通常の値を返すイテレータの場合、for-ofとfor-await-ofは同じ動作をします。
Promiseオブジェクトを返すイテレータの場合
次にPromiseオブジェクトを返すイテレータに、for-ofとfor-await-ofを適用するケースを考えてみます。
ここではブラウザでボタンを押されることで履行されるPromiseオブジェクトを作成します。
まずはボタンをHTML上で作成します。
<button id="btn">click</button>
次に、Promiseオブジェクトを作成する関数を作成します。
// DOMが構築されるまで待つ window.addEventListener( "DOMContentLoaded" , ()=> { const btn = document.getElementById("btn"); function clickPromise(t){ return new Promise( ( resolve , reject ) => { btn.addEventListener("click",function clickEvent(){ btn.removeEventListener("click",clickEvent); resolve(t); }); }); } // 続きのコードはここに挿入 });
clickPromise関数は複数回呼び出すことを想定しています。
addEventListenerは呼ばれるたびにリスナーを追加していくので、removeEventListenerで解除しています。
次にobjのイテレーターで、Promiseオブジェクトを返すように変更します。
const obj ={ data1:"A", data2:"B", data3:"C", *[Symbol.iterator](){ yield clickPromise(this.data1); yield clickPromise(this.data2); yield clickPromise(this.data3); } };
このオブジェクトにfor-ofを適用してみます。
for ( let data of obj){ console.log( data ); // 結果: Promise { <state>: "pending" } // Promise { <state>: "pending" } // Promise { <state>: "pending" } }
ボタンを押す前に、ログが表示されてしまいました。
イテレーターがPromiseオブジェクトを返し、それを表示しているだけなので、この結果は予想通りです。
では次に、for-await-ofに適用してみます。
async function func(){ for await ( let data of obj){ console.log( data ); } } func();
clickボタンを押してみてください。
押すたびに、"A","B","C"と順番に表示されるはずです。
つまりイテレーターから返されたPromiseオブジェクトの結果を待って、その結果を変数dataに格納しているのです。
for-await-ofの使用目的
for-await-ofは非同期の処理を順番に実行するために使用します。
例えば上の例のように、ボタンが押さるのを待ち、押されたら次のボタンが押されるのを待つ、のような処理です。
なお複数の非同期処理を順番に実行する必要がない、または並列に実行したいときはPromise.all()またはPromise.allSettled()を使用します。
- Promise.all()
引数で与えられた複数のPromiseオブジェクトの全てがresolveすると、Promise.all()の結果がresolveになる。
またどれか一つでもrejectされると、結果がrejectになる。 - Promise.allSettled()
引数で与えられた複数のPromiseオブジェクトの全てがresolveまたはrejectした時点で、resolveになる
resolveまたはrejectに関わらず、全ての結果を待ちたいときはPromise.allSettled()を使用します。
非同期処理の並列実行例:
HTML
<button id="btn1">click</button> <button id="btn2">click</button> <button id="btn3">click</button>
JavaScript
window.addEventListener( "DOMContentLoaded" , ()=> { const result={}; function clickPromise(id){ const btn = document.getElementById(id); return new Promise( ( resolve , reject ) => { btn.addEventListener("click",function clickEvent(){ console.log("click:" + id); btn.removeEventListener("click",clickEvent); result[id]=true; resolve(id); }); }); } const arry = []; arry.push( clickPromise("btn1") ); arry.push( clickPromise("btn2") ); arry.push( clickPromise("btn3") ); async function func(){ await Promise.allSettled(arry); Object.keys(result).forEach(e=>console.log(e + ":" + result[e])); } func(); });
Symbol.asyncIterator
for-await-ofの使用を前提としてオブジェクトにイテレーターを組み込む場合、[Symbol.iterator]ではなく[Symbol.asyncIterator]を使用することになっています。
ただし[Symbol.asyncIterator]が定義されていない場合は、[Symbol.iterator]が使用されます。
これまで使用してきたオブジェクトに[Symbol.asyncIterator]を定義して、どちらが呼ばれるか確認してみます。
const obj ={ data1:"A", data2:"B", data3:"C", *[Symbol.iterator](){ yield clickPromise(this.data1); yield clickPromise(this.data2); yield clickPromise(this.data3); }, *[Symbol.asyncIterator]() { yield clickPromise("async_" + this.data1); yield clickPromise("async_" + this.data2); yield clickPromise("async_" + this.data3); }, }; async function func(){ for await ( let data of obj){ console.log( data ); // 結果: Promise { <state>: "pending" } // Promise { <state>: "pending" } // Promise { <state>: "pending" } } } func();
実は[Symbol.iterator]を採用した場合、その関数を内部的に非同期イテレータに変換しています。
逆に考えると、[Symbol.asyncIterator]には非同期イテレータを指定する必要があります。
const obj ={ data1:"A", data2:"B", data3:"C", *[Symbol.iterator](){ // [Symbol.iterator] : function* () の短縮 yield clickPromise(this.data1); yield clickPromise(this.data2); yield clickPromise(this.data3); }, async *[Symbol.asyncIterator]() { // [Symbol.iterator] : async function* () の短縮 yield clickPromise("async_" + this.data1); yield clickPromise("async_" + this.data2); yield clickPromise("async_" + this.data3); }, }; async function func(){ for await ( let data of obj){ console.log( data ); } } func();
clickボタンを押すと、"async_A"、"async_B"、"async_C"が順番に表示されます。
記事の内容について

説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
ご意見はこちら。
https://affi-sapo-sv.com/info.php
【お願い】

■このページのURL
■このページのタイトル
■リンクタグ