jQueryコードリーディング: bind(),live(),delegate()

おはようございます。コードリーディング第四回です。

jQueryには、特定のエレメントにイベントを割り当てるメソッドとしてbind,live,delegateが存在します。今回は、これらのメソッドの実装を追ってみたいと思います。

対象

jQuery 1.5.0

はじめに

bind,live,delegateそれぞれの公式リファレンスは以下です。

また、それぞれのメソッドの違いを解説したエントリは以下です

簡単にまとめると、

.bind( eventType, [ eventData ], handler(eventObject) )
指定したエレメントにイベントハンドラを登録する。ready,click,... といったイベント登録メソッドは内部的にbindを呼んでいる
.live( eventType, handler )
ドキュメントルート(documentエレメント)にイベントハンドラを登録する。バブリングしてきたイベントがパターン(セレクタ、イベントの種類)に一致したらイベントハンドラが起動される。bindと異なり、イベント登録後に追加されたエレメントに対しても有効。
.delegate( selector, eventType, handler )
liveに似ているが、ドキュメントルートの代わりに$(...)で指定したエレメントに対してイベントハンドラを追加する。

  • liveは実際にはドキュメントルートに対する操作なのでわかりにくいし無駄な処理が走るのでdelegateのほうがいい
  • bindは後から追加したエレメントに対してイベントが登録されないし対象となるエレメントの数だけイベントハンドラが登録されるのでdelagete/liveのほうがいい

という解説がされていました。

はじめる前に:記述ルール

jQuery本体のコメントと区別するため、筆者が追加したコメントは//# の形式で記述します。

//# 筆者が追加したコメント
// 元からついていたコメント
function foo() {
}

bind()を読んでみる

function handler() { /*...*/ };
$('a').bind('click',handler);

というコードがどう処理されるか見てみましょう。

最初の呼び出し、$('a')については前回のエントリを参照してください。結果として、documentに対して'a'というセレクタで検索した結果を格納したjQueryオブジェクトが返ります。

さて、そのオブジェクトに対してbind('click',handler)すると何が起こるか。
.bind() | jQuery API Documentation

//# L:2971
//# だいたい同じだけど微妙に違うメソッド、bindとoneを一度に登録していますね
//# [http://api.jquery.com/bind/]
//# [http://api.jquery.com/one/]
//# one()のほうは、エレメントごとにイベントが一回しか呼ばれないようです。
jQuery.each(["bind", "one"], function( i, name ) {
	jQuery.fn[ name ] = function( type, data, fn ) {
		// Handle object literals
		//# bind({click: onClick, hover: onHover,...}) みたいな
		//# 呼び方ができるようですね。
		if ( typeof type === "object" ) {
			for ( var key in type ) {
				this[ name ](key, data, type[key], fn);
			}
			return this;
		}

		//# 引数の並びにいろんな意味があるためこんなことに
		if ( jQuery.isFunction( data ) || data === false ) {
			fn = data;
			data = undefined;
		}

		//# oneの場合はunbind処理を追加する。詳しくは追いません
		var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
			jQuery( this ).unbind( event, handler );
			return fn.apply( this, arguments );
		}) : fn;

		//# unloadの場合は一回しか呼ばれないようにする
		if ( type === "unload" && name !== "one" ) {
			this.one( type, data, fn );

		} else {
			//# で、これがメインの処理。
			//# thisに含まれている要素(今回は'a'にマッチした要素)に対して、
			//# イベントを登録していきます
			for ( var i = 0, l = this.length; i < l; i++ ) {
				jQuery.event.add( this[i], type, handler, data );
			}
		}

		return this;
	};
});

というわけで、各要素に対してjQuery.event.add()を使用してイベントハンドラを登録する処理でした。
jQuery.event.addの詳細についてはかなり複雑そうなので今回はパスします。

live()を読んでみる

function handler() { /*...*/ };
$('a').live('click',handler);

はどう処理されるのでしょう。

まずはドキュメントルート以下の'a'というパターンにマッチした要素を格納したjQueryオブジェクトを作成し、それにに対してlive('click',handler)の呼び出しを行います。
.live() | jQuery API Documentation

//# L:3085
//# live(),die()を一括で登録しています
jQuery.each(["live", "die"], function( i, name ) {
	jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) {
		//# 今回の例では、types='click', data=handler,fn=origSelector=undefined
		//# という引数で呼ばれます。
		var type, i = 0, match, namespaces, preType,
			//# origSelectorが指定されていないため、this.selectorが参照されます
			//# this.selectorについては前回読んだpushStackメソッドでも登場しました。
			//# http://d.hatena.ne.jp/gnarl/20110226/1298731211
			//# 上のエントリにおいて、$('a')が、$(document).find('a')を返すことを見ました。
			//# $(document).selectorは""です(L:106, L:202より)
			//# $(document).find('a').selectorは"a"です(L:250および$(document).selector=""より)
			//# というわけで、ローカル変数selectorには"a"が格納されます
			selector = origSelector || this.selector,
			//# $(document).contextはdocumentになります(L:107)
			//# $(document).find('a').contextもdocumentになります(L:247)
			//# よって、ローカル変数contextにはjQuery(document)が入ります
			context = origSelector ? this : jQuery( this.context );

		//# イベントタイプにオブジェクトを渡すことで複数のイベントを一括指定できる
		//# 今回は関係なし
		if ( typeof types === "object" && !types.preventDefault ) {
			for ( var key in types ) {
				context[ name ]( key, data, types[key], selector );
			}

			return this;
		}

		//# 第二引数がイベントハンドラだった場合の処理
		//# fnにイベントハンドラが設定される
		if ( jQuery.isFunction( data ) ) {
			fn = data;
			data = undefined;
		}

		//# bind()と違い、イベントタイプを空白で区切って複数指定できる
		types = (types || "").split(" ");

		//# iがはるか上で定義されてるのでわかりにくいことこの上ないな。
		//# 0で初期化されてるふつうのループカウンタです
		while ( (type = types[ i++ ]) != null ) {
			//# var rnamespaces = /\.(.*)$/ (L:2077)
			match = rnamespaces.exec( type );
			namespaces = "";

			//# ネームスペースの処理。
			//# 今回の例ではネームスペースを使用していないので関係なし
			if ( match )  {
				namespaces = match[0];
				type = type.replace( rnamespaces, "" );
			}

			//# hoverイベントとはmouseenter+mouseleaveの別名(see http://api.jquery.com/hover/ )
			if ( type === "hover" ) {
				types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
				continue;
			}

			preType = type;

			//# liveMap(L:3078)の定義に従って一部イベント名の変換をしているのだが、
			//# なぜイベント名によって処理が違うのだろう。
			//# focus/enterは指定されたイベント名に加えてfocusin/focusoutで登録、
			//# mouseenter/mouseleaveについてはmouseover/mouseoutに変換している
			if ( type === "focus" || type === "blur" ) {
				types.push( liveMap[ type ] + namespaces );
				type = type + namespaces;

			} else {
				type = (liveMap[ type ] || type) + namespaces;
			}

			//# ループ先頭からここまで、イベントタイプ名の処理
			if ( name === "live" ) {
				// bind live handler
				//# 今回のcontextは$(document)なので、
				//# イベント登録がdocumentエレメントに対して実行される
				for ( var j = 0, l = context.length; j < l; j++ ) {
					//# liveConvert(L:3234)
					//# 今回の場合はliveConvert('click','a') == 'a' となる
					//# documentエレメントに対して、イベント名'live.a'でイベントハンドラを登録
					//# event.addの実装については今回は触れない
					jQuery.event.add( context[j], "live." + liveConvert( type, selector ),
						{ data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
				}

			} else {
				// unbind live handler
				context.unbind( "live." + liveConvert( type, selector ), fn );
			}
		}

		return this;
	};
});

イベント名への操作を除けば、documentに対してイベントハンドラを登録するだけですね。

delegate()を読んでみる

function handler() { /*...*/ };
$(document).delegate('a','click',handler);

はどう処理されるのでしょう。

まず最初に$(document)が呼ばれ、その結果に対してdelegate('a','click',handler)が実行されます。
.delegate() | jQuery API Documentation

//# L:3004
jQuery.fn.extend({
	//# ...
	//# L:3021
	delegate: function( selector, types, data, fn ) {
		return this.live( types, data, fn, selector );
	},

短かい!!live()に委譲しているだけですね。

というわけで、

$(document).delegate('a','click',handler);

$(document).live('click',handler,undefined,'a');

と等価です。
liveの第四引数(origSelector)は先ほど使用されませんでしたが、指定された場合this.selectorではなく'a'がセレクタとして使用されます。

まとめ

live()とdelegate()はほぼ同じですが、live()の場合はセレクタが実際に実行されるため無駄な処理が走りパフォーマンス的に不利です。

var target=$('a'); //# 'a'を実際に検索する
target.live('click',handler); //# しかし、live()の中で参照されるのはセレクタ'a'であり、検索結果は使われない!

var target=$(document);
target.delegate('a','click',handler); //# 'a'での検索が発生しない

また、live/delegateイベントハンドラをひとつしか登録しませんがbind()は対象となるエレメントの数だけ登録します。

という、先述のブログ記事での主張を確認することができました。

jQuery.event.addが実際にどうやってイベントを登録しているか。また、セレクタでバブリングしてきたイベントをどうフィルタリングしているのか。あたりについてはまたの機会にしておきます。それでは。