JavaScriptは如何にしてAsync/Awaitを獲得したのか Qiita版

  • 805
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

JavaScriptは如何にしてAsync/Awaitを獲得したのか - がおさんち 技術部屋
※事前に↑の記事は読まなくても大丈夫です

という記事を、以前に個人ブログの方に書いたのですが、私も今年からはQiita始めたので、この記事をリファインして再度書いてみようと思います。

また、この記事では↑の記事では書ききれなかった話もいくつか増やしています。

例えば、不定回数実行されるPromiseの話だとか、非同期処理における例外処理周りの面倒くさい話だとか。

そういうちょっとだけ高度な話も混ぜつづ、前回書いたものよりもクオリティを上げるのを目標にします。それではいきます。

第一章 ~人類はsetTimeoutを採用しました~

古代のJavaScriptで、以下のような処理をしたい場合、どうしていたでしょうか。

ブラウザ更新直後に『a』を表示し、その2秒後に『b』を表示し、更にその1秒後に『c』を表示する

これの回答には、以下のような例も考えられると思います( :warning: 真似しないでください)

脳筋ループの例
var oldDate = new Date();

//更新後、即表示
console.log("a");

//2秒間すぎるまで無限ループ
while((new Date() - oldDate) < 2000);
oldDate = new Date();

console.log("b");

//1秒間すぎるまで無限ループ
while((new Date() - oldDate) < 1000);
oldDate = new Date();

console.log("c");

しかし、このコードには非常に大きな問題があるということが分かると思います。

というのも、JavaScriptは基本的にシングルスレッドである。
従って、このwhileループを回している間、他の処理ができないのだ。
(さらに言えば、こんなことのためにCPU燃やすのはやめて差し上げろ)

これでは困るため、人類はこれに対処するために、setTimeout関数を生み出した。

setTimeoutは、第一引数にコールバック関数、第二引数に処理遅延するミリ秒を記述する。

setTimeoutの例
//更新後、即表示
console.log("a");

setTimeout(function(){
    // 2秒後に呼ばれる
    console.log("b");
    setTimeout(function(){
        // 1秒後に呼ばれる
        console.log("c");
    }, 1000); // 1000ms(=1秒)後に呼ぶ関数を登録する
}, 2000); // 2000ms(=2秒)後に呼ぶ関数を登録する

古代人はこのように、コールバック関数という道具を持ち出すことで解決した。

非同期的な処理に対して、「準備ができたタイミングで関数を呼ぶ」という概念を持ってきたのです。

第二章 ~コールバック関数が生み出した悲劇~

さて、これで動作上は問題が無くなった。
理論上は どんなコードでも書くことが出来るはずです。

しかし、まだ気になるところがあります。

そうです、ネストが深くなってしまったのです。

先程の例は簡単な例なので問題ないのです。
しかし、実際にこの手法で開発を続けると、深いネストに包まれて、最悪の場合死に至るのです。

先日、以下のような要件を満たすコードの書き方が分からないという話が実際にあったので、それを例にしたいと思います。

jQuery.ajaxを用いて、とあるURLに対して、空文字レスポンスが帰ってくるまで、パラメータ変化させながら何度もリクエストを送信したい

という要件です。
言葉での説明にピンとこない人のために、直感的な不正解コードを先に書いてみましょう。

不正解コードその1、気持ちはわかるがそもそも意図した通りに動かない例
var URL = "getData.php";
var count = 0;
var dataList = [];

// 無限ループでajax通信を行う
while(true){
    $.ajax({
        type: "POST",
        url: URL,
        dataType: "text",
        param: { count: count },
        success: function(data){
            // データが空っぽだったら終了するために無限ループから抜ける
            if(data === ""){
                break;
            }

            // データが入っていたら、dataListに内容を追加する
            dataList.push(data);
        }
    });

    // 次のajaxのために値を1足す
    count++;
}

// 以下でdataListを処理
不正解コードその2、一応動くけどネスト地獄で死ぬ例
var URL = "getData.php";

$.ajax({
    type: "POST",
    url: URL,
    dataType: "text",
    param: { count: 0 }, // 1回目はパラメータとしてcountに0を与える
    success: function(data1){
        // データが空っぽだったら終了するためにコールバックを呼ぶ
        if(data === ""){
            callback([]); // 前回までのデータを全て入れておく(初回なので空)
            return;
        }
        $.ajax({
            type: "POST",
            url: URL,
            dataType: "text",
            param: { count: 1 }, // 2回目はパラメータとしてcountに1を与える
            success: function(data2){
                // データが空っぽだったら終了するためにコールバックを呼ぶ
                if(data === ""){
                    callback([data1]); // 前回までのデータを全て入れておく
                    return;
                }
                $.ajax({
                    type: "POST",
                    url: URL,
                    dataType: "text",
                    param: { count: 2 }, // 3回目はパラメータとしてcountに2を与える
                    success: function(data3){
                        // 以下省略
                        // あと114514回ネストを書く
                    }
                })
            }
        })
    }
});

function callback(dataList){
    // 終了時に以下に記述してdataListを処理
}

さて、これでは困りましたね。

ところで、不正解例その2では、ソースコードをよく見ると再帰的に記述されていますね。
ということは、関数を分けてあげて、再帰的に処理をすれば書けるのではないでしょうか。

正解コード、再帰的に処理したコードの例
// sendData関数を呼び出してajax通信を開始する
sendData(function(dataList){
    // 終了時に以下に記述してdataListを処理
});

function sendData(callback){
    var dataList = [];

    // 初期値を0として再帰的に記述した関数を呼び出す
    sendDataRec(0);

    function sendDataRec(count){
        $.ajax({
            type: "POST",
            url: URL,
            dataType: "text",
            param: { count: count },
            success: function(data){
                // データが空っぽだったら終了するためにコールバック関数を呼ぶ
                if(data === ""){
                    callback(dataList);
                    return;
                }

                // データが入っていたら、dataListに内容を追加する
                dataList.push(data);

                // カウントを1足して再帰的に関数を呼び出す
                sendDataRec(count+1);
            }
        });
    }
}

これで完全に動きますね。

ここで、超重要ポイントは、実は再帰的処理を書いたところではありません。

本当の重要ポイントは

  • 引数で受け取ったコールバック関数を、非同期処理が完了したタイミングで呼び出す関数を定義する
  • 呼び出すコールバック関数には、処理後の結果となる変数を引数として渡している

という2点です。

先の例では

function sendData(callback){}

が1点目該当します。

// データが空っぽだったら終了するためにコールバック関数を呼ぶ
if(data === ""){
    callback(dataList); // ←ここで結果を引数として渡している!
    return;
}

が2点目に該当します。

この2点は後で話の中心になるのでよく覚えておいてください。

めでたしめでたし。
…ではないんですよね。かなりいい線までは来てはいるんですけどね。

実は、コールバック関数による呼び出しの本当の地獄は 複数の非同期処理を組み合わせた時 にその真価を発揮するのです(発揮しないでくれ頼む)

第三章 人類は非同期処理機構を発明しました

第二章で使用した例を改造します。

DOMContentLoadedのイベントが完了した3秒後に先ほどのajax通信を実行したい

と書き換えてみましょう。愚直に書くと以下のようになります。

DOMContentLoaded完了3秒後にajax通信を実行する愚直実装の例
window.addEventListener("DOMContentLoaded",function(eve){
    setTimeout(function(){
        sendData(function(dataList){
            // 終了時に以下に記述してdataListを処理
        });
    },3000);
},false);

function sendData(callback){
    // 第二章で書いた実装なので省略します
}

ネストが…深くなってる!!!

いや、落ち着いて欲しい。

DOMContentLoadedイベントや、setTimeoutに関しても、sendData同様に考えればいいのだ。

つまり、非同期処理が完了したら、結果が引数で渡されたコールバック関数を呼び出す関数でラップすれば解決するはずだ!!きっとそうに違いない!!!

DOMContentLoadedやsetTimeoutも引数で渡されたコールバック関数を呼び出す関数でラップした例
// あれっ!?ネスト減ってないよ!?
awaitDOMContentLoaded(function(data){
    awaitTimeout(function(data){
        sendData(function(data){
            // 終了時に以下に記述してdataを処理
        });
    });
});

function awaitDOMContentLoaded(callback){
    window.addEventListener("DOMContentLoaded",function(eve){
        // DOMContentLoadedが完了したら、コールバック関数を呼ぶ
        callback(); // 特に渡すものがないので「何もない」を渡す(何も渡さない)
    },false);
}

function awaitTimeout(callback){
    // 3秒経ったらコールバック関数を呼ぶ
    setTimeout(function(){
        callback(); // 特に渡すものがないので「何もない」を渡す(何も渡さない)
    },3000);
}

function sendData(callback){
    // 第二章で書いた実装なので省略します
}

…現実は厳しかった。これは無駄な処理だったのか。

しかし、これもやっぱりよく見て欲しい。
先ほどと違って、処理に法則性が生まれたのです!!!!

つまり

window.addEventListener("DOMContentLoaded",function(eve){
    setTimeout(function(){
        sendData(function(dataList){
            // 終了時に以下に記述してdataListを処理
        });
    },3000);
},false);
// ↑の時は引数の取り方がバラバラだったので
// 関数(引数,コールバック関数(引数){
//     関数(コールバック関数(){
//         関数(コールバック関数(引数){
//         });
//     },引数);
// },引数);
// 
// というちぐはぐな形をしていた

awaitDOMContentLoaded(function(data){
    awaitTimeout(function(data){
        sendData(function(data){
            // 終了時に以下に記述してdataを処理
        });
    });
});
// ↑の時は引数の取り方が統一されたので
// 関数(コールバック関数(引数){
//     関数(コールバック関数(引数){
//         関数(コールバック関数(引数){
//         });
//     });
// });
// 
// という風に、共通化された形になっている!

こういう風に、「関数」や「引数」にだけ着目して見てみると、見事に共通化されていることが分かるかと思います。

そして、これはプログラマの性なのですが

共通化されているものは、配列や繰り返し文で処理できるはず

と考えるのが自然です。やってみましょう。

共通化されている非同期処理をまとめて処理する
// serialAsync([
//     [非同期処理1, コールバック関数1],
//     [非同期処理2, コールバック関数2],
//     ...,
// ]);
serialAsync([
    [
        awaitDOMContentLoaded,
        function(data){
            // 特にすることがないので処理は書かない
        }
    ],
    [
        awaitTimeout,
        function(data){
            // 特にすることがないので処理は書かない
        }
    ],
    [
        sendData,
        function(data){
            // 以下に記述してdataを処理
        }
    ],
]);

function serialAsync(asyncList){
    // 先頭の非同期処理だけは実行する必要が有るため取り出しておく
    var firstAsync = asyncList.shift();
    // 先頭の非同期処理を用いてdeferredオブジェクトを生成する
    var deferred = createDeferred(firstAsync);

    // 残った非同期処理リストは、nextを利用して数珠つなぎにする
    var targetDeferred = deferred;
    for(var i=0;i<asyncList.length;++i){
        targetDeferred.next = createDeferred(asyncList[i]);
        targetDeferred = targetDeferred.next;
    }

    // 最後に、先頭の非同期の実行をする
    deferred.invoke();

    // 実行を数珠つなぎにするためのリストを作成する
    function createDeferred(asyncArg){
        // 引数を分解する
        var asyncFunc = asyncArg[0];
        var callback  = asyncArg[1];

        // ごぐありふれた空のオブジェクトを定義する
        var deferred = {};

        // 次のdeferredオブジェクトを入れるプロパティを定義(初期値はnull)
        deferred.next = null;

        // invokeメソッドでは、ラップされた非同期関数を実行できるようにする
        deferred.invoke = function(){
            asyncFunc(function(data){
                // 非同期関数が実行完了したらコールバック関数を呼び出す
                callback(data);

                // まだ非同期関数が残っていたら、それを処理する
                if(deferred.next){
                    deferred.next.invoke();
                }
            });
        };

        // このオブジェクトを返す
        return deferred;
    }
}

function awaitDOMContentLoaded(callback){
    // 先述の通りなので省略
}

function awaitTimeout(callback){
    // 先述の通りなので省略
}

function sendData(callback){
    // 先述の通りなので省略
}

serialAsyncの呼び出し部分だけを見てください。

見事にネストが解消できているのが確認できると思います!!!

serialAsync関数は、分かりやすさのために様々な実装を妥協していますが、多くの非同期処理を捌くライブラリは、この考え方を基本にしています。

第四章 ~jQuery.Deferredで書いてみる~

因みに、jQueryにもjQuery.deferredという非同期処理機構が存在します。

参考程度に、jQuery.deferredで先程の例を実装すると以下のようになります。

jQueryを利用した実装例
// jQueryDeferredだと、最初にDeferredオブジェクトを返す任意の関数を実行して
// あとはthenで繋ぐ
awaitDOMContentLoaded()
    .then(function(data){
        // 特にすることがないので処理は書かない
    })
    .then(awaitTimeout)
    .then(function(data){
        // 特にすることがないので処理は書かない
    })
    .then(sendData)
    .then(function(data){
        // 以下に記述してdataを処理
    });


function awaitDOMContentLoaded(){
    // 最初に$.Deferred();でdeferredオブジェクトを生成する
    var deferred = new $.Deferred();

    // あとはほとんど先述の通りだけど、
    // 完了時にcallbackの代わりにdeferred.resolve();を呼ぶ
    window.addEventListener("DOMContentLoaded",function(eve){
        deferred.resolve(); // 引数は「何もない」を渡す(何も渡さない)
    },false);

    // 最後に、この関数がdeferred.promise();を返すように書き換える
    return deferred.promise();
}

// 本家deferredではコールバック関数は受け取らない代わりに
// この関数が非同期処理関数であることを示すために、promise()を返すようにする
function awaitTimeout(){
    // 最初に$.deferred();でdeferredオブジェクトを生成する
    var deferred = new $.Deferred();

    // あとはほとんど先述の通りだけど、
    // 完了時にcallbackの代わりにdeferred.resolve();を呼ぶ
    setTimeout(function(){
        deferred.resolve(); // 引数は「何もない」を渡す(何も渡さない)
    },3000);

    // 最後に、この関数がdeferred.promise();を返すように書き換える
    return deferred.promise();
}

function sendData(){
    // 最初に$.deferred();でdeferredオブジェクトを生成する
    var deferred = new $.Deferred();

    // 以下長いので省略。
    // 完了時にcallbackの代わりにdeferred.resolve(dataList);を呼ぶ

    // 最後に、この関数がdeferred.promise();を返すように書き換える
    return deferred.promise();
}

書き方こそ違えど、先ほど自作したオレオレ非同期処理機構と、jQuery.Deferredは本質的にかなり似ていることが分かると思います。

また、jQuery.deferredだとコールバック関数が不要な関数の場合は、省略することが出来ます。

(※厳密に言うと、deferred.promise()の戻り値で、thenで繋がれている関数が、非同期関数かどうかを判定しているので、同期的なコールバック関数を常に挟む必要がなくなったのです)

jQueryによる改良非同期処理
awaitDOMContentLoaded()
    .then(awaitTimeout)
    .then(sendData)
    .then(function(data){
        // 以下に記述してdataを処理
    });

// 以下同じなので省略

最初の地獄と見比べると、かなりすっきりしてきましたね。

(補足) jQuery.ajaxも、実はjQuery.deferredで実装されています。

詳しい話が知りたい方は以下の記事等で。
おじさんが若い時はね$.ajax()のオプションでsuccessとかerrorとか指定していたんだよ(追憶)

本記事の構成上、先の例ではどうしてもsuccessとかerrorで書かないと破綻する部分があったのですが、実際はthenで繋げていきましょうという話です。

第五章 ~ES2015 Promiseで書いてみる~

さて、jQuery.Deferredはこれで古いブラウザでも統一的に書けて便利なのですが、いつまでもライブラリに頼る人生は辛いものがあります。

ES2015では、jQuery.deferred(が実装の元にした)、Deferred仕様を更に改良した、Promiseというものが実装されています。

現代人はこちらを使うのが一般的です。実際に書いてみましょう。

Promiseを利用した場合の実装例
// PromiseもjQueryDeferredの時と基本的に書き方は同じ
awaitDOMContentLoaded()
    .then(awaitTimeout)
    .then(sendData)
    .then(function(data){
        // 以下に記述してdataを処理
    });


function awaitDOMContentLoaded(){
    // Deferredを作成せずに、Promiseオブジェクトそのものを返す
    return new Promise((resolve,reject)=>{
        // あとはほとんどDeferredと同じだけど、
        // 完了時にdeferred.resolveの代わりにresolve();を呼ぶ
        window.addEventListener("DOMContentLoaded",function(eve){
            resolve(); // 引数は「何もない」を渡す(何も渡さない)
        },false);
    });
}

// 本家deferredではコールバック関数は受け取らない代わりに
// この関数が非同期処理関数であることを示すために、promise()を返すようにする
function awaitTimeout(){
    // Deferredを作成せずに、Promiseオブジェクトそのものを返す
    return new Promise((resolve,reject)=>{
        // あとはほとんどDeferredと同じだけど、
        // 完了時にdeferred.resolveの代わりにresolve();を呼ぶ
        setTimeout(resolve,3000);
    });
}

function sendData(){
    // Deferredを作成せずに、Promiseオブジェクトそのものを返す
    return new Promise((resolve,reject)=>{
        // 以下長いので省略。
        // 完了時にdeferred.resolveの代わりにresolve(dataList)を呼ぶ
    });
}

基本的にはjQuery.Deferredの時と似てるので普通に処理が追えるかと思います。

因みに補足ですが、Promiseの第二引数は、失敗時に呼ぶreject関数です。

今までの問題は簡単にするために全て成功するという前提で書いてましたが、実際は非同期処理が失敗するケースだって存在しえますからね。

さて、Promise(やjQuery.Deferred)を用いることで、ネストが完全に消えました。

めでたしめでたし。

…だったら良かったのですが、実はまだ完全にめでたしというわけにはいかなかったのです。

第六章 ~Promiseに潜む闇~

実は、二章で紹介した再帰の問題に対して、Promiseだけでは解決が非常に困難という問題がまだ残っています。

問題を簡単にするために、以下のようなプログラムについて考えてみましょう。

Promiseを使って、90%の確率で成功し、10%の確率で失敗する関数を1秒毎に実行してください(60秒間)

これを実装しようとしたら、おかしなことになります。

then地獄
delayAndRandom()
    .then(delayAndRandom)
    .then(delayAndRandom)
    .then(delayAndRandom)
    .then(delayAndRandom)
//  ...省略 (thenを合計59回書くことで、60秒経過する)
    .then(()=>{
        // 全てresolveが呼ばれた場合、この関数が実行される
        console.log("全部成功したよ!");
    })
    .catch(()=>{
        // 途中1回でもreject関数が呼ばれた場合、この関数が実行される
        console.log("途中で失敗したみたい…");
    });

function delayAndRandom(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            // 90%の確率で0以外が入り、10%の確率で0が入る
            const isSuccess = Math.random()*10|0;

            // デバッグ時は以下のコンソールを差しこんでおくと状況を把握しやすいかもです。
//          console.log((isSuccess) ? "成功したっ" : "失敗した…");

            // 成功時はresolveを呼び、失敗時はrejectを呼ぶ
            (isSuccess) ? resolve() : reject();
        }, 1000);
    });
}

・・・お分かりいただけただろうか?

Promiseには、繰り返し実行するような非同期処理に対して弱い、という致命的な弱点を抱えています。

Promiseだけでこの問題を解決しようとするならば、どうしても一つ上位の再帰的関数でラップする必要が出てきます。

やはり避けられない再帰的関数の宿命
// 再帰的な処理でラップした関数を呼ぶことで解決する
delayAndRandomN(60) // 60秒なので60を渡す
    .then(()=>{
        // 全てresolveが呼ばれた場合、この関数が実行される
        console.log("全部成功したよ!");
    })
    .catch(()=>{
        // 途中1回でもreject関数が呼ばれた場合、この関数が実行される
        console.log("途中で失敗したみたい…");
    });

function delayAndRandomN(limit){
    // 0秒の時は即完了させる
    if(limit--) return Promise.resolve();

    return new Promise((resolve,reject)=>{
        // 内部再帰的関数を実行する
        delayAndRandomRec();

        // 実際に再帰する内部関数を定義する
        function delayAndRandomRec(){
            // とりあえず実行して1秒待ってみる
            delayAndRandom()
                .then(()=>{
                    // まだ実行回数が残っていたら再帰的に呼び出す。
                    // 実行回数が残ってなければ、全部成功したので完了させる。
                    (limit--) ? delayAndRandomRec() : resolve();
                })
                // 1度でも失敗した時は即失敗扱いとして終了させる
                .catch(reject);
        }
    });
}

function delayAndRandom(){
    // こっちはさっきと実装同じなので省略
}

あれっ…Promiseつらい… :cry:

面倒なので書きませんが、第二章で示したajax通信の再帰的処理のケースにおいても同様です。

というより、有限回数(Nが十分に大きい)なら頑張ればfor文に書き換えられますが、不定回数なら確実に再帰じゃないと無理なので事態は更に深刻です。

Promiseで不定回数を回すと地獄です。あなたは再帰を強いられます。

しかし何故こんなことが起きるのか?その理由にはPromiseの存在背景自体に有ります。

というのも、Promiseの考え方自体が関数型言語の文化のモナドパターンの一つ(継続モナド)にかなり近いのです。

この時点で、手続き的な記述も出来るJavaScriptに搭載するには厳しいものがあったのです。for文使わせてくれ。

第七章 ~手続き的にPromiseと共存する世界~

もちろん関数型の人間であればPromiseを愛して終了なのですが、多くのJavaScriptエンジニアは手続き的な書き方が恋しいはずです。

ではJavaScriptエンジニアが欲しがる手続き的な書き方とはどんなものだろうか、仮想的に書くとこうなる。

顧客が必要だったタイプのPromiseの手続き的処理
new Promise((resolve,reject)=>{
    // 60秒だから60回実行するfor文
    for(let i=0;i<60;++i){
        try{
            // delayAndRandomがresolveされるまで完全にこの行で止められる
            // 夢のsleep関数
            sleep(delayAndRandom);
        }
        catch(e){
            // rejectされた時は、例外として飛ぶので、それを外側のPromiseに伝える
            reject();
        }
    }
    // for文を抜けたら、全て成功したことを意味するので、外側のPromiseに伝える
    resolve();
}).then(()=>{
    // 全てresolveが呼ばれた場合、この関数が実行される
    console.log("全部成功したよ!");
}).catch(()=>{
    // 途中1回でもreject関数が呼ばれた場合、この関数が実行される
    console.log("途中で失敗したみたい…");
});

function sleep(promise){
    // どうにかしてここを実装したい。
}

function delayAndRandom(){
    // こっちはさっきと実装同じなので省略
}

こういう書き方が出来たら、手続き人間にとっては幸せの極みだろう。
しかし、当然問題になってくるのはsleep関数の実装方法である。どうあがいても無理だろうか。

・・・いや、出来る。

ES2015 期待の新星、Generator/Iteratorを利用すれば、このsleep関数でやりたいことが実現できるのです。

Generator/Iteratorを利用した、手続き的なPromiseの処理
asyncFunc(function* (){
    // 60回Promiseを待機する。
    // rejectされた時は、自動的にこの関数が中断されるからtry-catchで囲む必要もない。
    for(let i=0;i<60;++i){
        yield delayAndRandom();
    }
}).then(()=>{
    // 全てresolveが呼ばれた場合、この関数が実行される
    console.log("全部成功したよ!");
}).catch(()=>{
    // 途中1回でもreject関数が呼ばれた場合、この関数が実行される
    console.log("途中で失敗したみたい…");
});

function asyncFunc(gen){
    return new Promise((resolve,reject)=>{
        // ジェネレータからイテレータを作成する
        const iter = gen();

        // 内部再帰的関数を実行する
        asyncFuncRec();

        // 実際に再帰する内部関数を定義する
        function asyncFuncRec(arg){
            try{
                // イテレータを一つ進めてPromiseを1つ取り出す
                // その際に、ひとつ前のPromise実行時の結果を次のPromiseに与える
                const {value: promise, done} = iter.next(arg);

                // 終了条件を満たしていたら、resolveとする
                if(done){
                  resolve(arg);
                  return;
                }

                promise
                    // resolveされた際に、この関数を再帰的に呼ぶためにthenでつなげる
                    .then((data)=>{
                        asyncFuncRec(data);
                    })
                    // rejectされた際には、その結果をこの関数自体の失敗として扱い、橋渡す
                    .catch((err)=>{
                        reject(err);
                    });
            }
            catch(err){
                // 実行中に何らかの同期的なエラーが発生したら失敗として扱い、橋渡す
                reject(err);
            }
        }
    });
}

function delayAndRandom(){
    // こっちはさっきと実装同じなので省略
}

かなりすっきりしました。

delayAndRandomの内部にはPromiseで実装されているので、Generator/Iteratorと、Promiseが協力しているようにも見えますね。

このように、Generator/Iteratorを利用することで、再帰を書かないといけなかった部分を 、「asyncFunc」という関数に共通部品として外部に追いやることが出来ます。

そして、面倒な部分を共通部分として取り出すということは、つまりエンジニアはこのasyncFuncの部分をコピペするだけで

Generatorを利用した記述で、不定回数実行するPromiseも手続き的に処理ができるようになる

ということです!!!

もっとも、このasyncFuncがやっていることは、既に偉い人がライブラリ化していますがね。
気になる人は tj/co - GitHub へ。

最終章 ~これから歩むES.Nextとasync/await~

まだ完全には確定した仕様では無いのですが、ES.nextにはasync/awaitという機構が既に提案されています。執筆現在でStage 3なのでほぼ仕様確定状態です。

これが実装されれば、七章で書いたasyncFuncの部分を、まるごと言語仕様として利用できるので、coのようなライブラリに依存する必要もなくなります。

async-awaitで光の実装
// asyncFunc関数に渡してたジェネレータを、async関数にする
(async ()=>{
    // 60回Promiseを待機する。
    // rejectされた時は、自動的にこの関数が中断されるからtry-catchで囲む必要もない。
    for(let i=0;i<60;++i){
        // yieldと書いてた部分をawaitにする
        await delayAndRandom();
    }
})().then(()=>{
    // 全てresolveが呼ばれた場合、この関数が実行される
    console.log("全部成功したよ!");
}).catch(()=>{
    // 途中1回でもreject関数が呼ばれた場合、この関数が実行される
    console.log("途中で失敗したみたい…");
});

function delayAndRandom(){
    // こっちはさっきと実装同じなので省略
}

無駄な物が完全になくなり、やりたいことだけを書けるようになりましたね!
これが人類が求めていたゴール地点ですよ。最高です…

せっかくなので最後は二章で出したajaxの例で書いて締めくくりましょうか。

jQuery.ajaxを用いて、とあるURLに対して、空文字レスポンスが帰ってくるまで、パラメータ変化させながら何度もリクエストを送信したい

でしたね。

async-awaitで幸せな非同期通信ライフ
(async ()=>{
    const dataList = [];
    const url = "getData.php";

    for(let i=0;;i++){
        // 通信してresolveされるまで待機する
        const text = await ajaxPost({
            url,
            dataType: "text",
            param: { count: i },
        });

        // 得られたメッセージが空であれば終了する
        if(text === "") break;

        dataList.push(text);
    }

    // for文を無事抜けたら成功したことを意味する
    return dataList; // 硬派な書き方が好きな人は「return Promise.resolve(dataList);」
})().then(dataList=>{
    // ここでdataListを処理する
}).catch(err=>{
    // 何らかの原因で通信に失敗したらここで処理する
    console.error("通信に失敗しました…");
    console.error(err);
});

function ajaxPost(prop){
    return new Promise((resolve,reject)=>{
        $.ajax(Object.assign({ type: "POST" }, prop))
         .then(resolve)
         .fail(reject);
    });
}

最高の非同期ライフになることを祈ります。