A Day in the Life

2008-01-27

AS3 でイベントやファンクションのチェイン

AS3 でイベントチェインでよしなにしたい時って、結局非同期な並列処理の時が多い。それ以外は普通に関数で回すとわかりにくくなるので OO った方がよいかなー、と自分の中では落ち着いてる。特に無名関数の適用は、関数の引数を予めきちんと書かなくてはならなくて(...restでもいいけど)面倒だったりする。

AS3 では無名関数を使うところでJSより便利だなー、というところはあまり無い。そもそも組み込みの引数に無名関数渡すAPIが使いにくい。Array#forEach なんかは三つも引数書くのが冗長…。for each あるしね。実装側も無名関数が渡ってきて apply で適用しようとしても引数の型があわないとエラーになるし。無名関数の引数の数とかいちいち重いE4Xリフレクション使わないでも解りたい。あとバインドメソッドという無名関数とは違うメソッドクロージャ(this が確実にそのインスタンスに束縛される)を意識せずに使うことが出来るので、これでまた無名関数の利用価値が減ってしまった。唯一意識的に使うのはスコープあたりでゴニョゴニョしたい時かな。

話は飛んで Deferred の概念的なものは Event/EventDispatcher で代用することができて。AS3 では EventDispatcher が組み込みであって、かつだいたいの非同期処理はイベント終了時に大概なんらかのイベントを送出する。ほとんどの場合は Event.COMPLETE だ。足りないのは DeferredList 的なもの。並列処理をスムーズに行いたいよね。

で、組み込みの Event/EventDispatcher を使ってなんかうまく DeffererdList っぽいこと出来ないかな、と思ってて以前作ってほったらかしにしていた Chain クラスがあったのでちょっと書き直してそれなりに使えるようにしてみた。

関数とイベントをチェインで繋げる FunctionChain/EventChain 、並列実行の ParallelChain がメインとして利用する。

(new FunctionChain(function(f:FunctionChain):void {
    log('1');
})).
chain(new FunctionChain(function(f:FunctionChain):void {
    log('2');
})).
start();
// 1
// 2

chain の値渡しは Chain#results に値を入れる。これは非同期での実行の場合、return で返すとうまく渡せないためである。一つ前のチェインは parent で参照できる。parent って名前はどうなんだ。

(new FunctionChain(function(f:FunctionChain):void {
    f.results = 'hello';
})).
chain(new FunctionChain(function(f:FunctionChain):void {
    log(f.parent.results + ' world')
})).
start();
// hello world

FunctionChain とか書くのはめんどくさいので Chain#f で繋げたりも出来る。

(new Chain).f(function(f:FunctionChain):void {
    f.results = 'hello';
}).
f(function(f:FunctionChain):void {
    log(f.parent.results + ' world')
}).
start();

FunctionChain の第二引数には自分でこのチェインが終わったか明示的に通知するよフラグをつけれれる。この場合 FunctionChain#notify() で通知できる。たとえば Tweener の動作が終わったらチェインを進めたい場合は次のようにする。

(new Chain).f(function(f:FunctionChain):void {
      Tweener.addTween(shape, {
          time:1,
          alpha:0,
          onComplete: f.notify // Tweener は完了時に onComplete で指定された関数を実行する
      });
}, true). // 第二引数に true
f(function(f:FunctionChain):void {
    log('finish');
}).
start();

次は EventChain。EventChain では第一引数に IEventDispatcher、第二引数にチェイン実行時の関数、第三引数に待ち受けるイベント名(デフォルトではEvent.COMPLETE)を指定する。たとえば以下のコードでは HTTP でデータ取得した後に FunctionChain が実行される。

var c:Chain = new Chain();
c.chain(new EventChain(
    new URLLoader, function(c:Chain, loader:URLLoader):void {
        loader.load(new URLRequest('http://paprika/sleep.rb?sleep=3')) // HTTP で取得するのに3秒かかるURL
        c.results = loader;
    }
)).
f(function(f:FunctionChain):void {
   log(f.parent.results.data);
}).
start();

EventChain では IEventDispatcher を汎用的に扱うことが出来、次のチェインに結びつけられる。たとえば Timer を使って、次のチェインまで5秒待つコードは次のようになる。ちなみに new EventChain を書くのが面倒な時は e で繋げられる。

Timer はタイマーの終了時に TimerEvent.TIMER_COMPLETE を投げるのでそれを第三引数に書く。

var c:Chain = new Chain();
c.e(new Timer(5000, 1), function(c:Chain, t:Timer):void {
    log('wait start');
    t.start();
}, TimerEvent.TIMER_COMPLETE).
f(function(f:FunctionChain):void {
    log('5 sec');
}).
start();

待ったり、ループ回したりするのはよく使うので DelayChain/LoopChain として組み込みでつくっておいてある。

new FunctionChain(function(f:Chain):void {
    log('1');
})).
f(function(f:Chain):void {
    log('2');
}).
delay(1000).
loop(3).
f(function(f:Chain):void {
    log('finish');
}).
start();

を実行すると次のような動作になる。

1
2
// 1 秒待つ
1
2
// 1 秒待つ
1
2
// 1 秒待つ
finish // ループ三回終わったので次のチェインへ

Chain#stop によるイベントの中断も出来る。

var c:Chain = (new FunctionChain(function(f:Chain):void {
    log('1');
})).
delay(200).
loop().
f(function(f:Chain):void {
    log('finish');
}).
start();
(new DelayChain(500)).f(function(f:Chain):void {
    log('stop!');
    c.stop();
}).
start();

を実行するとこんな感じ。止めてるので次へ続かない。

1
1
1
stop!

最後に parallel での並列処理が終わってからの実行。

var c:Chain = new Chain();
c.parallel(
    new EventChain(
    new URLLoader, function(c:Chain, loader:URLLoader):void {
        loader.load(new URLRequest('http://paprika/sleep.rb?sleep=3'))
        c.results = loader;
    }),
    new FunctionChain(function(f:FunctionChain):void {
        f.results = 1;
    }),
    new DelayChain(1000)
).f(function(f:FunctionChain):void {
    log(f.parent.results);
})
.start();

三つの処理、HTTPでデータ取ってくる処理、関数実行、1秒待機が並列で実行され、終わったら次のチェインへ。結果は ParallelChain#results に格納されてるので

[
Object data=Sleep: 3.0\nSleep: Finish\n bytesLoaded=25, // URLLoader
1, // 関数実行した results
undefined // DelayChain は results に値を入れてないので
]

な配列みたいな感じで。これで画像を全部並列で全部ロードし終わったところで表示処理を開始する、なんかも適当に書ける。

と勢いで作ってみたけど実際にどれぐらい便利かは不明なのでちょこちょこ自分で使ってみようと思う。というか説明ながくてしかもわかりにくい…。

カヤック行ってきた

明日の日曜日は soozy conf ということであえて土曜日にカヤックに行ってきた!新オフィスいいなー仕事はかどりそう。ドンブリカフェも思ってた以上に良かった。yksk と二人ハッカソンした。休日なのに久しぶりにコード書きがはかどったよ!ごちそうさまでした><。typo さんには先日のコリンのアレですごい久しぶりにあったんだけどここ10日で3回も会ってる!!!

記事の一覧 >