jQueryのbind/live/delegateの違いまとめ、と新API .on()の使い方

jQueryのイベント記述方法はいくつかあり、大雑把におさらいしたのが以下の3パターンです。

  1. まず一番基本的なのが $("a").click(fn) や $("a").bind('click', fn) です。click(fn)はbind('click', fn)の省略形です。
  2. 次にjQuery1.3で $("a").live("click", fn) という機能が出来ました。liveの良いところはDOM操作で出たり消えたりするエレメントに対して再バインド無しでイベント定義ができる利便性と、内部的にはbind個所がdocumentの1か所になり複数個所へのbindが無くなることによるメモリ効率と実効速度の向上です。
  3. 更にjQuery1.4.2で $("#foo").delegate("a", "click", fn) という書き方が出来るようになりました。これは特定要素以下に限定するliveイベント定義のようなもので、範囲を限定することによるパフォーマンス向上効果があります。あとliveでは登録時の先頭の$("a")で不必要な要素検索が一度実行されてしまう無駄がありましたがそれが無ありません。$(document).delegate("a", "click", fn)は$("a").live("click", fn)と同じ効果です。

特徴を纏めると↓こんな感じのハズ。

  • パフォーマンスは delegate > live > bind で優れていて*1
  • DOM操作耐性は live > delegate >> bind
  • 実行順序は bind > delegate > live *2

これらに加えて今回追加されるのが次に説明する新APIのon/offです。

APIは.on()と.off()

jQuery 1.7b1で更に .on/.off という新しいイベントAPIが追加されました*3
これは機能的には特に目新しいものはありませんが、これまでに bind/unbind, live/die, delegate/undelegate という6つにも増えてしまったイベントAPIの記述をシンプルに纏めてくれるものです。
また、それぞれの記述方法が混在した際の微妙な不具合(混乱)の対策でもあります。混在した際の不具合というのは例えば以下のようなもので、いずれも記述に対して直感と反する動作が起こることが原因で開発者を混乱させました。

  • $(document).unbind("click") すると $("#hoge").live("click", fn) などで定義したイベントも消えてしまう*4
  • div > aという親子関係に対して $("a").live("click", fn1) と $("div").bind("div", fn2) があった際にfn2→fn1の順で実行されてしまう*5


これまでのイベントAPIを新APIで書き換えるとどうなるか、比較したのが以下の表です。

API API 速度 DOM操作耐性 実行順
$(elems).bind(events, fn) $(elems).on(events, fn) × 1
$(elems).unbind(events, fn) $(elems).off(events, fn) - - -
$(elems).delegate(selector, events, fn) $(elems).on(events, selector, fn) 2
$(elems).undelegate(selector, events, fn) $(elems).off(events, selector, fn) - - -
$(selector).live(events, fn) $(document).on(events, selector, fn) 3
$(selector).die(events, fn) $(document).off(events, selector, fn) - - -

ずいぶん分かりやすくなりました。
一番大きな効果はAPIが覚えやすくなったことだと思います。特にunbind系の、delegateに対するundelegateはともかくliveの逆はdieとか、覚えわけが面倒だったのが一掃されたのが嬉しいです*6
次にバインド対象とセレクタの関係が直感的になったことです。これにより $(document).unbind(event) もとい.off()で、予想外にliveイベントが消えてしまうことも無くなる筈です。何故なら旧liveイベントは $(document).on(event, fn) と書くようになるので、$(document).off(event) でそのイベントが外されることは実に直感的で当然のことになるから。

実験

.on()の使い方実習と、bind/live/delegateの動作のおさらいも兼ねてサンプルページを作ってみました。

イベントのバインド部分は以下のようになってます。

$(".foo button").on("click", function(){ $("#log").append("<div>bind</div>")});          //旧.bind()
$(".foo").on("click", "button", function(){$("#log").append("<div>delegate</div>")});    //旧.delegate()
$(document).on("click", ".foo button", function(){$("#log").append("<div>live</div>")}); //旧.live()

HTML構造としては、.fooの内側にCLICK MEボタンが居て、再構築1では.fooの中身を消して作り直し、再構築2では.foo自体を消して作り直しをしています。
それを踏まえてDOMの状態を変えながら「CLICK ME」を押してみてください。
初期状態では bind/delegate/live 全てのイベントが動き、
再構築1を押した後では delegate/live だけが、
再構築2を押した後では live のみが実行されることが確認できると思います。

感想

  • 覚えることが減った上に直感的に分かりやすくなった!これに尽きますね。とりあえず僕はこのAPIが気に入りました。まだβですが多分変更は無さそうな気がするので新しいサイトではどんどん使ってみようかなと思ってます。
  • liveイベントの記述が毎回documentを書かなきゃいけないのがちょっとダルイ。$.on(events, selector, fn)とかいう風に書けたら楽なんだがなぁ。


おわり。

*1:http://bit.ly/pNBiEn ←ここページのRun testsボタンを押すと自分のブラウザでベンチマークができます。昔は何となく対象DOMに直接bindした方がliveとかより速いと思ってたんだが全く逆だったので印象に残ってる。delegateやliveはDOM操作でイベント消えたりしないわ速いわで最強だった。

*2:bindはイベント発生元に一番近いので一番最初に実行されます。delegateで設定した要素は必ずイベント発生源の親か祖先要素なので、発生源からバブリングしてきたイベントは次にdelegateでセットしたコールバックを実行します。全ての祖先要素をたどってバブリングしてきたイベントは最後documentに辿り着きます。liveハンドラはdocumentにbindされるので必ず最後の実行になってしまいます。

*3:追加と言うよりは置き換えという表現のほうが正しいかも。実際bind/unbind/live/die/delegate/undelegateの実装は全て内部でon/offを呼びなおすだけになっていました。

*4:内部を考えればliveは実はdocumentにbindしてるのでまぁ当然といえば当然ですが、実際遭遇したら混乱すること請け合いです。

*5:一見内側の$("a")に設定されたfn1の方が先に実行されそうですが、実際はliveの場合は実際は$("a")ではなくdocumentにバインドされるのでこのような直感との逆転現象が起こります。

*6:onでイベント登録とか、Nodeっぽいのも最近Nodeを良く使う身としては覚えやすくてGOODだと思います。