jQuery の bind, unbind の裏側

jQuery のソースを呼んでいて、イベント登録のところが複雑だったので備忘録として記しておく。

バージョンは 1.2.1。

そもそもの目的

DOM 標準の removeEventListener は、element と type(click, submit, blur など) と listener の3つを指定する必要がある。

element.removeEventListener(click, listener, false);

jQuery ではイベント解除に unbind という便利な関数が用意されている。

  1. element, type, listener を指定して解除する(通常の removeEventListener と同じ)
  2. element, type を指定して全てのイベントハンドラを解除する
  3. element から全てのイベントを解除する

例えば、

$("#foo").unbind();

とすると、#foo なエレメントに(jQuery 経由で)bind したイベントを全て解除できる。

1. は通常の removeEventListener そのままなんだけど、2. や 3. をどうするかが腕の見せ所。

実現方法

3. を実現するためには、ある element に割り当てられた全てのイベントハンドラを知れる必要がある。これを実現するために、element をキーにしたハッシュを作成することにする。

このハッシュを、仮に hash としておく。(実際には、element[(new Date()).getTime()] に作成される)

bind の裏側

bind は次のようにして使う。

$("foo").bind( type, fn );

このとき、裏側では次のように動いている。

  1. fn に guid を設定する。guid はイベントハンドラに一意につけられた id。
  2. type と guid をキーにして、イベントハンドラを保持する(hash.events[type][guid] = fn)。
    • element から登録された type 一覧を知ることができる
    • element と type から登録された fn の一覧を知ることができる
  3. element に type のイベントが登録されていない場合は、element.addEventListener(type, <特製ハンドラ>, false) する。
    • 複数回 bind しても、登録されるイベントハンドラはこの特製ハンドラ1つだけ
    • このハンドラが呼ばれると、hash.events[type] に保存されている fn(実際に bind で登録されたイベントハンドラ)を順番に呼び出す。

element と type が同じなら、1回しか addEventListener が呼び出されず、同じイベントハンドラを使いまわしている。

unbind の裏側

  • element, type, fn を指定した場合
    1. hash.events[type] から fn の guid を削除する
    2. hash.events[type] が空になったときは、removeEventHandler する
  • element, type を指定した場合
    1. hash.events[type] を空にする
    2. removeEventListener する
  • element のみ指定されたとき
    1. hash.events から全ての type を列挙して、removeEventListener する
    2. hash.events も初期化する

element と type について登録されているハンドラ数が 0 になったときのみ、removeEventListener が走る。複数のハンドラが登録されている場合は、ハッシュを修正するだけで OK。

さらに

さらにもうちょっと面白いのが、addEventListener で登録される特製ハンドラは、element に対して同じ関数が使い回されている。

特製ハンドラはイベントが実行されると、

  1. 第一引数(event)から event.type でイベントの種類を調べる。
  2. hash.events[event.type] で実際に呼び出すべき関数一覧を取得する。
  3. これを順番に呼び出す。

この仕組みのおかげで、element が分かれば、実際に addEventListener されている特製イベントハンドラを割り出せるわけだ。removeEventListener するときに、type によって実装を変える必要がなくなる。

で、これとは別に、イベントハンドラに追加の data を渡す仕組みや、クロスブラウザの仕組みなどが入っていて、かなり複雑。

あとがき

分かりやすく書こうと思ったけど複雑ですね…。

ソース読んだほうが早いよ!という人は、直接 ソース(event.js) を読んでみたほうがよいかも。bind から呼ばれるのが add、unbind から呼ばれるのが remove ですよ。