同期・非同期構文

【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"が順番に表示されます。

更新日:2021/01/11

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。