MochiKit.Async.Deferredで非同期処理の同期処理を直感的に書く

LDRize minibuffer reblog commandは中身の95%がTumblrにポストするJSActionsスクリプト、2007/6/10版 – 実用で出来ていて、今後のメンテナンス性(というかいかに自分は何もしないかという意味だけど)を考えるとTomblooのコードベースに移行しないといけないのでTomblooのコードを読みました。0.0.9のやつです。

Tombloo、0.0.7 – 実用に15日かけてリファクリタングしたと書かれていますが、読んで感動。ShareOnTumblrのときは大きく変わって各サービスをみんなおんなじインターフェイスでtumblrにpostできるような抽象化層が作られていて、ぜんぶ extract -> post っていうメソッド呼び出しで完結するようになってました。readしてwriteすれば実体がなんであろうと読めて書けるみたいなかんじです。

実体はウェブ上のサービスなので、ぜんぶの操作がMochiKit.Async.Deferredでラップされていて、これがどういう仕組みなのかを知るところから入りました。探しかたが悪いのかもしれないけど、まともなドキュメントがなくて(MochiKit自体のドキュメントがぜんぜんどう便利なのかが表現できてない)、けっきょくTomblooのコードをまねして自分で書いてみてDeferred感動。

というわけで、具体的にどういうときに便利でどう書くのかを教えてくれないとわかんない自分のようなやつにもわかるように書いてよ!とアタマにきたので自分だったらわかりそうなやつを書きます。

問題定義 – Flickrの写真を複数のところにポストしたい

はじめに、ひとつのFlickrの写真をTumblrとFFFFOUND!のふたつにpostする手順を説明して、それをjavascriptで書くならどうなるかを考えます。

Flickrのページで見た写真をTumblrにpostするには

    1. 写真のpermalinkを取得する(ブラウザで見ている写真のURLはほか(のIP?)からは見えない)
    2. 写真のメタデータを取得する
  1. 写真のpermalinkとメタデータが揃うのをまつ
    1. Tumblrにpostする
    2. FFFFOUND!にpostする

という手順で行います。図だとこう。

flickr to tumblr and FFFFOUND! flow

これをベタなjavascriptで書くなら(APIのendpointとかいろいろてきとうです)

var flickrResultSet = {
    permalink: null,
    metadata: null
};

GM_xmlhttpRequest( {
    url: "http://flickr.com/api/getPermalink",
    onload: function (permalink) {
        flickrResultSet.permalink = permalink;
    }
} );

GM_xmlhttpRequest( {
    url: "http://flickr.com/api/getMetadata",
    onload: function (metadata) {
        flickrResultSet.metadata = metadata;
    }
} );

var postResultSet = {
    tumblr: null,
    ffffound: null
}

var waitFlickrComplete = setInterval( function () {
    if ( flickrResultSet.permalink && flickrResultSet.metadata ) {
        GM_xmlhttpRequest( {
            url: "http://tumblr.com/share",
            onload: function (res) {
                postResultSet.tumblr = res;
            }
        } );
        GM_xmlhttpRequest( {
            url: "http://ffffound.com/post",
            onload: function (res) {
                postResultSet.ffffound = res;
            }
        } );

        var waitPostComplete = setInterval( function () {
            if ( postResultSet.tumblr && flickrResultSet.ffffound ) {
                alert("post complete!");
            }
        }, 100);
        clearInterval(waitFlickrComplete);
    };
}, 100);

こんなかんじでsetTimeoutXMLHttpRequestの結果が揃うのを待って、揃ったらさらに次の処理を並列で呼び出してその結果が揃うのをまたsetTimeoutで待ちます。非同期処理を直列に繋いでいくにつれてだんだんネストが深くなっていきます。

上のコードはエラー処理をしていませんが、実際のコードはonerrorで帰ってきたときも考える必要があるのでコードはさらに複雑になってきます。

これを簡単に記述するというのがMochiKit.Async.Deferredの役割です。こういう非同期処理の結果が揃うのを待つバリアを簡単に記述できて、それを直感的な書き方でもって直列に繋いでいくことができます。

MochiKitの場合

というわけでMochiKitの場合。はじめに4つのリクエストを出して、それが揃うのを待ってから2つのリクエストを出す例です。図にするとこう。

Picture 3-7

書いてある数字はそのリクエストのレスポンスが帰ってくるまでに何秒かかるかを意味しています。3secのやつは3秒後にリスポンスが帰ってきます。

この処理をMochiKitではDefferedを使って以下のように書けます。

var elapsed = ( function () {
    var start = null;
    return function () {
        if (!start)
            start = Date.now();
        return ( (Date.now() - start) / 1000)
    }
} )();

console.log(elapsed(), "start" );

var dl1, dl2;

dl1 = new DeferredList( [
    doXHR('/sleep.php?n=3').addCallback( function (res) {
        console.log(elapsed(), "n=3", res, res.responseText);
        return res.responseText;
    } ),
    doXHR('/sleep.php?n=4').addCallback( function (res) {
        console.log(elapsed(), "n=4", res, res.responseText);
        return res.responseText;
    } ),
    doXHR('/sleep.php?n=5').addCallback( function (res) {
        console.log(elapsed(), "n=5", res, res.responseText);
        return res.responseText;
    } ),
    doXHR('/sleep.php?n=6').addCallback( function (res) {
        console.log(elapsed(), "n=6", res, res.responseText);
        return res.responseText;
    } )
] ).addCallback( function (res) {
    console.log(elapsed(), "first DeferredList complete.", res);

    return dl2 = new DeferredList( [
        doXHR('/sleep.php?n=1').addCallback( function (res) {
            console.log(elapsed(), "n=1",res, res.responseText);
            return res.responseText;
        } ),
        doXHR('/sleep.php?n=2').addCallback( function (res) {
            console.log(elapsed(), "n=2", res, res.responseText);
            return res.responseText;
        } ),
    ] ).addCallback( function (res) {
        console.log(elapsed(), "second DeferredList complete.", res);
        console.log(elapsed(), "last", res);
        return res;
    } )
} ).addCallback( function (res) {
    console.log(elapsed(), "end", res, dl1, dl2);
} );

実行した結果はFirebugのconsoleに出てきます。

firebug console log

(注: デフォルトだと同一サーバには同時に2つのリクエストしか出ないのでnetwork.http.max-persistent-connections-per-serverを8にして測定してあります)

結果を見るとわかりますが、はじめのDeferredList(dl1)に入れてある4つのdoXHR全てから結果が帰ってきた時点で次のaddCallbackの引数になっている関数が呼び出されています。
ここがちょっとわかりにくかったのですが、ある処理が終わってからまた並列で処理を行いたいときにはaddCallbackの中でDeferredListのインスタンスをreturnすることで実現できます。上の例では/sleep.php?n=1/sleep.php?n=2に対して並列でリクエストを送るDeferredList(dl2)returnしています。

そうすると次に続くaddCallbackで渡された関数は、このDeferredList(dl2)に含まれている処理が全て終わってから呼ばれるようになります。

感覚的には、とにかく前の非同期処理が終わってから実行したいという境界ごとに関数にして、それをaddCallbackで繋げてあげるかんじです。もし並列に実行して全部の処理が終わるのを待ってから実行したいというときにはaddCallbackの引数にDeferredList(もしくはDeferred)を返す関数を渡してあげればいいみたいです。

書いてみるとちっともわかりやすくなってないし、あらためて

を読むと、はじめからそう書いてあるし、まだDeferredの持ってる特徴の一部しかわかってないかんじですが、実例と数字と結果が出てると少しは分かりやすいかなと思ったのと、とにかくさっくり直感的に書けちゃうところに感動したということで….


About this entry