人生ずっと勉強

人生ずっと勉強ですね。 https://twitter.com/KiyotakaGoto

brook の実装読んでみた

brook という、非同期処理をシーケンシャルに記述できる javascriptフレームワークを最近使う機会があったので、どういう実装になってるのかを読んでみた。
(ちなみに iPhonechrome で github 開いてコードを読んだのですが、 iPhone で github 上のソースを読む時のベスプラとかあるんですかね・・・?)

そもそも brook ってなんぞ?は以下を。
tanabe/Advent-calendar-2011 · GitHub

brook どこにあるの?は以下を。
https://github.com/hirokidaichi/brook

※brook は、 Namespace という、javascript名前空間を提供するライブラリに依存しています。

実装読む前にそもそもどう使うのか

たとえば、setTimeout で、1秒毎に挨拶文を変更するサンプルコードなんかは、以下のように書けます。

Namespace('kiyotaka.goto.test')
.use('brook promise')
.define( function ( ns ) {
    
    var first = ns.promise( function ( next, parameter ) {
        setTimeout( function () {
            $('#greeting').html('good morning');
            next( 'good afternoon' );
        }, 1000 );
    });
    
    var second = ns.promise( function ( next, greeting ) {
        setTimeout( function () {
            $('#greeting').html( greeting );
            next( 'good evening' );
        }, 1000 );
    });
    
    var third = ns.promise ( function ( next, greeting ) {
        setTimeout( function () {
            $('#greeting').html( greeting );
        }, 1000);
    });
    
    ns.provide({
        init : function () {
            first.bind( second, third ).run();
        }
    });
});

Namespace
.use('kiyotaka.goto.test init')
.apply( function ( ns ) {
    ns.init();
});

動いている様子はこちら:2013-04-11 1st - jsdo.it - Share JavaScript, HTML5 and CSS
本来なら入れ子がかなり深くなって読みづらくなるこんな処理を、シーケンシャルな感じで書けちゃいます。

この brook がどういう実装になっているのかを今回、読んでいきたいと思います。
ソース:brook/src/brook.js at master · hirokidaichi/brook · GitHub

brook 読みました

brook のコードの全体は、大きく3つに分かれるかなぁと思いまして、上から

(※javascript にクラスの概念はないですが、便宜上、クラスって呼んじゃいます

  • prototype にメソッド生やしているところ
  • namespace にメソッド提供してるところ

って感じかなと思います。

まずは、サンプルコードでつかってる promise メソッドってなんぞや、ということで、
最後から見てみます。

    var promise = function(next,errorHandler){return new Promise(next,errorHandler);};
    ns.provide({
        promise : promise,
        VERSION : VERSION
    });

引数をそのままコンストラクタに渡して Promise クラスを new して返します。

コンストラクタ付近を見ると、

    var k       = function(next,val){ next(val); };
    var lift    = function(f){ return ( f instanceof Promise ) ? f : new Promise( f ); };
    var Promise = function(next,errorHandler){
        this.next = next || k;
        if (errorHandler)
            this.setErrorHandler(errorHandler);
    };

こんな感じ。this.next に、promise メソッドの第1引数に渡した関数オブジェクトが入る感じですね。
lift ってのは、渡されたオブジェクトを Promise オブジェクトにして返す関数みたいです。あとから出てきます。

で、メソッド生やしてるところにうつって、実際にどういう処理になってるか見てみます。
サンプルコードの first, second, third 変数には、ns.promise() の返り値を入れてるので、Promise クラスのインスタンスが入ってます。
それぞれ next プロパティに、ns.promise() に渡した関数オブジェクトが入ってます。
最後、first.bind( second, third ).run(); としてるので、 とりあえず run() を見てみます。

    proto.run = function(val){
        this.subscribe( undefined , val );
    };

subscribe というメソッドを呼んでます。
subscribe とはなんでしょう。今回、errorHandler は渡してないので、必要部分だけ見てみます。

    var empty = function(){};
    proto.subscribe = function(_next,val){
        var next = _next || empty;
        if( !this.errorHandler )
            return this.next(next,val);
        (中略)
    };

this.next() ( ns.promise() に渡した関数オブジェクト )を実行してますね。
名前の通り、 run() によって、this.next() が実行されるんですが、
直接的には subscribe() が this.next() を実行するようです。
そしてこの this.next() の第1引数に、シーケンシャルに処理を実行していってるときの「次の」関数に相当する
関数オブジェクトを渡し、第2引数に this.next() 自身が利用するパラメータを渡しているようです。

では、どうやって非同期処理をシーケンシャルに実行することを実現しているのか、次は bind() を見ていきます。

    proto.bind = function(){
        var r = this;
        for( var i = 0,l = arguments.length;i<l;i++ ){
            r = r.concat( lift(arguments[i]) );
        }
        return r;
    };

引数の数だけ concat() を繰り返して、最後に結合しきった変数 r を返してますね。
concat() という名前からして、シーケンシャルな香りがします。
lift はクラス定義付近で見たように、引数を( Promise のインスタンスでなければ)Promise のインスタンスにして返す関数でした。
なので、「bind メソッドを呼んだ Promise インスタンスに、引数で渡した Promise インスタンスたちをくっつけていってる」みたいな感じですかね。
サンプルコードでいうと、first に second, third をくっつけてる、っていう感じ。

concat() を見ます。

    proto.concat = function(after){
        var before = this;
        var next   = function(n,val){
            before.subscribe(after.ready(n),val);
        };
        return new Promise(next);
    };

concat() を呼び出した Promise インスタンスを「before」、引数に与えた Promise インスタンスを「after」としてますね。
first.bind( second, third ) が呼び出され、concat に second が渡されたときだと、
before は first, after は second ですね。
最終的に concat 自身は新しく Promise インスタンスを作って返してます。
そして新しい Promise インスタンスの this.next() は、変数before( firstに相当 ) の subscribe を実行します。
subscribe はすでに見たように、
「this.next() の第1引数に、シーケンシャルに処理を実行していってるときの「次の」関数に相当する関数オブジェクトを渡し、第2引数に this.next() 自身が利用するパラメータを渡」します。

ready() を見ると、

    proto.ready = function(n){
        var promise = this;
        return function(val){
            promise.subscribe(n,val);
        };
    };

こうなっています。だいぶややこしくなってきました(個人的に)。

なので、流れの整理のため、仮に third を bind せず、first.bind( second ).run(); だったとすると、
この時点でどういう感じになるかというと、

run()
--this.subscribe(undefined, undefined)
----first.subscribe( second.ready( undefined ), undefined );
------first.subscribe( function( val ) { second.subscribe( undefined, val ); }, undefined );
--------first.next( function( val ) { second.subscribe( undefined, val ); }, undefined );

こんな感じですね。
first.next は、サンプルコードの

function ( next, parameter ) {
    setTimeout( function () {
        $('#greeting').html('good morning');
        next( 'good afternoon' );
    }, 1000 );
}

なので、これの next に「function( val ) { second.subscribe( undefined, val ); }」が、
parameter に「undefined」が入ります。
すると、next( 'good afternoon' ); の部分はつまり、
second.subscribe( undefined, 'good afternoon' );
ということなので、たしかに first と second がシーケンシャルに実行されます。

concat って要するに、

first.subscribe( function( val ) {
    second.subscribe( fonction ( val ) {
        third.subscribe( function ( val ) {
        }, paramForThird);
    }, paramForSecond);
}, paramForFirst );

ってなるように(つまりサンプルコードを brook 使わずに書いたようになるように)、Promise インスタンスを構築していくものみたいです。

こうして、非同期処理をシーケンシャルに書けるようにしているようです。

(最後力尽きて適当になってしまった・・・そのうち加筆するかも。
(※間違いなどあるかもしれません。