同期・非同期構文

【JavaScript】 非同期イテレータと非同期ジェネレーターについて理解しよう

更新日:2021/08/25

JavaScriptは反復処理を非同期におこなう非同期イテレータという機能があります。
そこで通常のイテレータとの違いを含めて非同期イテレータについてお伝えします。
また発展形である非同期ジェネレーターについてもお伝えします。

 

非同期イテレータとは

非同期イテレータは、通常のイテレータと同じようにnextメソッドを持ちます。
ただしnextメソッドが返す値が異なります。

nextが返す値

通常のイテレータ{ value:任意値 , done:真偽値 }
非同期イテレータ{ value:任意値 , done:真偽値 }で解決するPromiseオブジェクト

通常のイテレータは、nextメソッドを呼び出すと{ value:任意値 , done:真偽値 }形式のオブジェクトを返します。
doneの値がtrueになるまで繰り返します。

通常のイテレータの例


function myIterator(){
    return {
        count:0,
        next() { //   next:function(){ の短縮系
            return this.count < 3
                ? { value : this.count++ , done:false}
                : {  done:true };
        }
    }
}
function func1(){

    let iterator = myIterator();
    let result = iterator.next();

    while( !result.done ){
        console.log( result.value );
        result = iterator.next();
    }
}
func1();

実行すると、コンソールに1,2,3とログ表示されます。

非同期イテレータは、nextメソッドを呼び出すとPromiseオブジェクトを返します。
Promiseが解決すると{ value:任意値 , done:真偽値 }形式のオブジェクトを返します。

次の例はブラウザ上のボタンが押されるのを待つ非同期イテレータを作成しています。

非同期イテレータの例


window.addEventListener( "DOMContentLoaded" , ()=> {

    function myAsyncIterator(){
        return {
            count:0,
            next() { //   next:function(){ の短縮系

                return new Promise( resolve =>{
                    if( this.count >= 3 ) resolve( { done: true } );
                    else{
                        const count = this.count ++;
                        const btn = document.getElementById("btn");

                        btn.addEventListener("click",function clickEvent(){
                            btn.removeEventListener("click",clickEvent);
                            resolve( { value : count , done: false} );
                        });
                    }
                });
            }
        }
    }
    async function func2(){

        let asyncIterator = myAsyncIterator();
        let result = await asyncIterator.next();

        while( !result.done ){
            console.log( result.value );
            result = await asyncIterator.next();
        }
    }
    func2();
});

HTML


<button id="btn">click</button>

clickを押して動作を確認してみてください。
1,2,3と順番に表示されるはずです。

上記のコードではasyncおよびawaiキーワードを使用しています。
これらについては次のページを参考にしてください。
【JavaScript】 async/awaitを解説します

 

asyncキーワードとイテレーター

非同期関数のおさらい

通常の関数にasyncキーワードを付加すると、非同期関数が生成されます。
非同期関数は実行すると、戻り値がPromiseオブジェクトでラップされます。

通常関数と非同期関数


function func(){
    return 1;
}
console.log( func() ); // 出力結果:1

async function aFunc(){
    return 1;  // new Promise( 1 )が返る
}
aFunc()
    .then( value => console.log( value )); // 出力結果:1

イテレータにasyncを付加して非同期イテレータ化

イテレータのnextメソッドにasyncキーワードを付加すると、通常の関数と同様に戻り値がPromiseオブジェクトでラップされます。
これにより、非同期イテレータとして機能します。

イテレータの非同期イテレータ化


function myIterator(){
        return {
            count:0,
            async next() {

                    // 戻り値がPromiseオブジェクトでラップされる
             return this.count < 3
                ? { value : this.count++ , done:false}
                : {  done:true };
                });
            }
        }
}

非同期関数内で、他の非同期処理の結果を待つ場合はawaitキーワードを使用します。

非同期関数内でawaitキーワードを使用


function myAsyncIterator(){
    return {
        count:0,
        async next() { //   next:function(){ の短縮系

                if( this.count >= 3 )  return { done: true } ;
                else{
                    const count = this.count ++;
                    const btn = document.getElementById("btn");

                        // クリック待ち
                    await new Promise( resolve => {
                        btn.addEventListener("click",function clickEvent(){
                            btn.removeEventListener("click",clickEvent);
                            resolve( );
                        });
                    });
                    return { value : count , done: false};
                }
        }
    }
}

 

イテラブルなオブジェクト

上の例のイテレータ関数をオブジェクトに実装して、イテラブルなオブジェクトを作成してみます。

通常のイテラブルなオブジェクトは、[Symbol.iterator]プロパティにイテレータ関数をセットします。

for-of構文を使用すると内部でnextメソッドが呼び出され、返された値が順番に変数に格納されます。

通常のイテラブルなオブジェクト


const obj1 ={

    [Symbol.iterator]:myIterator,

};
function func1(){
    for  ( let data of obj1){
        console.log( data );
    }
}
func1();

[Symbol.asyncIterator]プロパティに非同期イテレータ関数をセットすると、非同期のイテラブルなオブジェクトとして扱われます。

繰り返し処理にはfor-await-of構文を使用します。

非同期のイテラブルなオブジェクト


const obj2 ={
    data1:"A",
    data2:"B",
    data3:"C",

    [Symbol.asyncIterator]:myAsyncIterator,

};
async function func2(){
    for await ( let data of obj2){
        console.log( data );
    }
}
func2();

func2関数のfor-await-ofは、最初にobj2の [Symbol.asyncIterator]プロパティから非同期イテレーターを取得します。
次にnextを実行してPromiseオブジェクトを取得します。
Promiseの結果を待ち、受け取ったオブジェクト ({ value:任意値 , done:真偽値 })のvalueプロパティを変数dataにセットします。

再度nextを呼び出し、doneがtrueになるまで繰り返します。

 

非同期ジェネレーター

ジェネレーター関数の前にasyncを付加すると、非同期ジェネレーターが生成されます。

async function* 関数名(){ }

通常のジェネレーター関数はyieldキーワードで値を返しますが、非同期ジェネレーター関数はPromiseオブジェクトを返します。
Promiseが解決すると非同期イテレータと同様に{ value:任意値 , done:真偽値 }形式のオブジェクトを返します。

本当にそうなのか確認してみます。

非同期ジェネレーターの動作確認


async function* asyncGenerator(){
    yield 1;
}
function fun1(){
    const g = asyncGenerator();
    g.next().then( e=>console.log(e) ); // Object { value: 1, done: false }
    g.next().then( e=>console.log(e) ); // Object { value: undefined, done: true }
}
fun1();

g.next()がPromiseオブジェクトを返すので、その値に対してthenメソッドを実行することで、Promiseの結果を取得できます。
想定通りの動作をしていることから、非同期ジェネレーターが{ value:任意値 , done:真偽値 }で解決するPromiseを返していることがわかりますね。

次はyieldでPromiseオブジェクトを返してみます。
前述の例の結果から、{ value:任意値 } にはPromiseオブジェクトがセットされることが予想できます。

ブラウザ上でボタンを押されるまで待つ非同期ジェネレーター


function btnWait(n){

      // Promiseオブジェクトを返す
    return new Promise( resolve => {
        const btn = document.getElementById("btn");
        
        btn.addEventListener("click",function clickEvent(){
            btn.removeEventListener("click",clickEvent);
            resolve(n);
        });
    });
}

async function* asyncGenerator(){
    yield btnWait(1); // btnWaitの戻り値はPromise
}
function fun1(){
    const g = asyncGenerator();
    g.next().then( e=>console.log(e) );
    g.next().then( e=>console.log(e) );
}
fun1();

clickを押してみてください。
次のような結果になるはずです。

{ value: 1, done: false }
{ value: undefined, done: true }

yieldでPromiseオブジェクト返した場合、そのPromiseオブジェクトの結果を待ってくれるのです。

また、非同期ジェネレーターはそのまま非同期のイテラブルなオブジェクトに適用できます。

非同期ジェネレーターを非同期のイテラブルなオブジェクトに適用


const obj = {
    [Symbol.asyncIterator]:asyncGenerator,
}
async function fun1(){
    for await( let data of obj){
        console.log( data ); // 結果: 1
    }
}
fun1();

更新日:2021/08/25

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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