MENU

JavaScript構文同期・非同期

【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


■このページのタイトル


■リンクタグ