written by mizchi at 小物エンジニアの会.
- ゲームエンジニア
- 仕事 JavaScript/AS3/C#
- 最近 League of Legends しかしてない
- この発表は省力モードです(gist)
- 原稿消えた + 前日糞眠かった
- スライドにするにはコード多すぎた
- サーセン
最高の夏 = 花火 = イベントディスパッチャー = オブザーバーパターン
購読と発火
よくあるjQueryのコード
<button id="clickme">click me</button>
$("button#clickme").on("click", function(e){
console.log("clicked", e);
});
- buttonをマウスでクリック
- DOMがclickイベントを発火
- 登録された関数をeventオブジェクトを引数に実行
DOMはイベントを発火する
今日はjQueryで解説するけど、各自適当なフレームワークに置き換えて読んで
$target = $("#target")
$target.on "myevent", (e) -> console.log "hello", e.type
$target.trigger 'myevent', {type:"myevent"}
イベントを実行する人が、イベント発火元を知らなくて良いようにする
購読者は発火元を知っているが、発火元は購読者が誰か知らなくて良い
=> だいたいにおいて特定の構造を要求してダックタイピングする
(タスクシステムのタスクがexecute要求する、みたいな話)
- HTML
- Flash
- Node/EventEmitter
- というかモダンなGUIライブラリだいたい全部
GUIをやるなら避けては通れない概念。 サーバーでもNodeのEventEmitter等で使うし多分いろいろある
何はともあれ聞いてくれ。今日ドラッグイベントからドラッグアンドドロップを実装したいとする。
標準のマウスイベントをハンドリングして、ドラッグアンドドロップのイベント名で発火しなおす
泥臭くて面倒臭い実装になるであろうフラグ処理や座標計算を、受け手では意識しないように必要な情報を受け取れるようにする 「うまく実装出来れば」 dispatcherとobserverが疎結合になり、コード完結になり、再利用しやすい。
(今回は面倒臭いので擬似コードだけで実装はしない)
ドラッグアンドドロップに最低限必要な情報は以下の3つ
- x : Number
- y : Number
まず擬似コードを書いてみる(jQuery/coffee)
$target = $("#target")
$area = $("#dragAndDropArea")
# ドラッグ領域がターゲットにイベントをdispatchするように登録する関数
$.registerDragAndDrop({
from: $area
to : $target
})
$target.css {backgroundColor:"blue"} # 最初は青
$target.on "dragstart", (e) ->
$target.css {backgroundColor:"red"} # 赤くする
$target.on "dragmove", (e) ->
$target.css {left:e.x, top:e.y}
$target.on "dragend", (e) ->
$target.css {left:e.x, top:e.y}
$target.css {backgroundColor:"blue"} # 青に戻す
こんだけできりゃ十分
(似非コードなので全然正しくないです)
使うマウスイベントは以下の三種
- mousedown
- mousemove
- mouseup
このとき内部的には以下の状態を持つ
- isDragging: 現在ドラッグ中か否か
- target : ドラッグする対象
イベントの受ける側は、内部状態を知りたくない(興味が無い -> 関心の分離)
実際のフローは以下のようになる
- 「開始条件」mousedown時に、もしtargetが現在の座標の上にかぶっていれば、targetをセットし、現在の座標を保存してdragstartイベントを発火
- 「更新」mousemove時に、もしisDragging == true ならば、座標計算してdragmoveイベントを発火
- 「終了条件」mouseup時に、もしisDragging == true ならば、dragendイベントを発火
じゃあ失敗すると…???
gotoになる
onClick = (e) ->
console.log("clicked", e);
$("button#clickme").on "click", onClick
$("button#clickme").on "hover", (e)->
onClick(e)
やることが一緒だからといって他人のコールバックを使ってはいけない! コードから意図が見えないので、読む人は「死ね」と呟きながらコードを辿ることになる ハハッ俺はやらないよ、と思ってるでしょ?こういうコードすごく多いから…
onClick = (e) ->
console.log("clicked", e);
initialize = ->
onClick({})
onXXX はコールバックであることを期待するけど、普通の関数のように使われてるケース。だいたい期待を裏切るので糞コード。 onXXXだとか, XXXhandlerってのがよくある命名規則。(スレッド使うコードはhandlerが多い気がする
onClick = (e) ->
console.log("clicked", e);
$("button#clickme").on "click", (e) ->
# 100行に渡る実装
基本的にはオブザーバーは「段階」を表現するので、MVCでいうとコントローラの役割をすることが多い。 要はコントローラに全部書くなということ。MVCの基本。
onClick = (e) ->
console.log("clicked", e);
execute = (e) ->
# event scheme に依存した処理
$("button#clickme").on "click", (e) ->
execute(e)
上のやつを改良したかのように見えて、実は何も改善していない。 コントローラとして必要な責務はパラメータの分解であり、eventオブジェクトと密結合なexecute関数はリファクタリングの余地が残っている。 逆に言えばパラメータの分解以外やるなということでもある。
onClick = (e) ->
console.log("clicked", e);
execute = (type) ->
#...
$("button#clickme").on "click", (e) ->
e.type = "hoge"
execute(e.type)
イベントは受け取り手で発行されたままの状態を維持することを期待する。OOP的に言うならgetterがあってsetterがない。
イベントディスパッチャーの実装によるが、各インスタンスへ発行するイベントが共有される場合、発火順に依存する副作用が発生する。
モンキーパッチならともかく、本番コードでこのコードを書く奴は万死に値する。
このコードをみてほしい。こいつをどう思う?
window.onload = ->
somethingBigAndTemporaryInstance = {...}
console.log somethingbigAndTemporaryInstance
$("button#clickme").on "click", (e) ->
console.log "hoge"
すごく…おおきいメモリリークです…
この somehitngBigAndTemporaryInstance は、実はGCによってリリースされない。即時関数のクロージャに引っかかって参照カウントが残っている。
最近の当たり前のようにモダンなクロージャがあるプログラミング環境ならやりがちなミスだが、発見も難しい。
RubyやC++11のlambdaはクロージャの名前空間を覗くスコープを定義できるので、こういうミスは減る。Java6(DalvikVM)にクロージャはない。
最初に翻訳パターンを解説したのは意味がある
$target = $("#target")
$target.on "a", -> $target.trigger 'c'
$target.on "b", ->
# ...
$target.trigger 'd'
$target.on "c", ->
# ...
$target.trigger 'b'
$target.trigger 'd'
$target.on "d", -> console.log "fire!!!"
$("#button").trgger "a"
# fire
# fire
読めますか?僕の悲しみがわかりますか?
僕は20段の絡み合ったイベントディスパッチャーをみたことがあります。
イベントが発火する様子
ご清聴ありがとうございました