brook の実装読んでみた
brook という、非同期処理をシーケンシャルに記述できる javascript のフレームワークを最近使う機会があったので、どういう実装になってるのかを読んでみた。
(ちなみに iPhone の chrome で 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 インスタンスを構築していくものみたいです。
こうして、非同期処理をシーケンシャルに書けるようにしているようです。
(最後力尽きて適当になってしまった・・・そのうち加筆するかも。
(※間違いなどあるかもしれません。