Mochikit で非同期な処理を逐次実行

hail2u.net - JSONPに同期リクエスト
snippets from shinichitomita’s journal - 非同期処理をシーケンシャルに扱うために

を読んで、以前 prototype.js ベースでメソッドを逐次実行する仕組みを作ったのを思い出しました (id:reinyannyan:20060223:p1, id:reinyannyan:20060227:p1)。

コマンド・パターンを利用して、コマンドを、それを管理するオブジェクト (センター、マネジャー) に預けることで、簡単に大量の逐次処理が出来るようにしたものです。

例えば

var x = new XMLHTTP;
large_url_list.each(function(url) {
  x.get(url, callback);
});

のような、一度に大量の HTTP リクエストをする様なケースで、呼び出し側はコマンドの仕組みについて全く意識することなく、順番に一つずつ処理をしていくことが可能になります。

(大量のメソッドが一度に全てオブジェクト化される、という点が欠点といえば欠点かも知れませんが)


一方、id:reinyannyan:20060911:p1 の livedoor clip の JSONP API のデモでは、JSONP リクエストを次のように行っていました:

exhaust(
    imap(
        function(id, seconds) {
            wait(seconds).addCallback(function() {
                getJSON({ livedoor_id:id, limit:10 });
            });
        },
        $("list").value.split(/\s+/),
        count()
    )
);

要は for ループで個々のリクエストを少しずつ setTimeout でずらしながら行う、ということです。

count 関数というカウント・アップをしてくれる関数を利用し、ループ毎に wait 関数での待ち時間を延ばしているわけです。

ただ、これはあくまでも擬似的な逐次処理であり、しかも一度に全ての関数が setTimeout と共に野に放たれる感じがちょっとダサいな、と思っていました。


ということで、本当に一つずつ、順番に非同期処理を行う関数を考えてみました。

function deferredSequence(dfunc, iterable) {
    var item = next(iterable);
    if (typeof(item) == 'undefined' || item === null) {
        return;
    }

    dfunc(item).addBoth(function () {
        deferredSequence(dfunc, iterable);
    });
}

第一引数には Deferred オブジェクト (非同期な処理をカプセル化するオブジェクト) を返す関数を取ります。こんな関数です:

function getJSON(api, cb, params) {
    var d = sendJSONPRequest(api+"?"+queryString(params), "callback");
    d.addCallback(cb);
    return d;
}

第二引数としてイテレータを取り、非同期な処理に渡すパラメータとします。

その非同期処理が完了すると再帰し、イテレータの次の要素の処理に移る、そしてそれが繰り返される、という仕組みです。

ゆっくりと回る for ループ、といった感じでしょうか。Array#each だとこういうことが出来ないので、Mochikitイテレータの便利さを痛感します。

(余談ですが、重いループ処理を一回ずつ setTimeout で間を空けて軽くする、といったこともこの手法の応用で可能になると思います)

使い方はこんな感じです:

deferredSequence(
    getJSON,
    imap(
        function(id) {
            return { livedoor_id:id, limit:10 };
        },
        $("list").value.split(/\s+/)
    )
);

シンプルですよね?

サンプルはこちらです