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+/) ) );
シンプルですよね?
サンプルはこちらです