jQueryコードリーディング:セレクタを渡した場合の挙動について - $('a') は何を返すか

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

これからエレメント操作系のコードを読んでいきたいのですが、そのための準備として、セレクタを引数にしてjQueryオブジェクトを構築したとき何が起こるか見ていきたいと思います。
セレクタの解釈にはSizzleというエンジンが使用されているようですが、今回はあまり深くは追いません。「セレクタを渡した場合どのようなオブジェクトが返ってくるのか」を理解することを目的とします。

対象

jQuery 1.5.0

これまでのエントリ

今回読むコード

$('a')

記述ルール

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

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

クエリセレクタをざっと読んでみる

$('a')

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


最初に実行されるのは$==jQuery関数。

// L:22
var jQuery = function( selector, context ) {
		//# selector==='a', context===undefinedである
		// The jQuery object is actually just the init constructor 'enhanced'
		//# rootjQuery===jQuery(document)と定義されている(L:1035)
		return new jQuery.fn.init( selector, context, rootjQuery );
	},

これはjQuery.fn.initをnewしてるだけなので、

// L:97
	init: function( selector, context, rootjQuery ) {
		var match, elem, ret, doc;

		//# いくつか条件判定が続くが、どれも引っかからないので省略

		// Handle HTML strings
		if ( typeof selector === "string" ) {
			// Are we dealing with HTML string or an ID?
			match = quickExpr.exec( selector );

			// Verify a match, and that no context was specified for #id
			if ( match && (match[1] || !context) ) {
				//# ここも引っかからない
			// HANDLE: $(expr, $(...))
			} else if ( !context || context.jquery ) {
				//# ここが実行される。
				//# jQuery(document).find('a')
				return (context || rootjQuery).find( selector );
			}
		//# 以下略
	},

最終的にjQuery(document).findが呼ばれてるのでfindを見る
.find() | jQuery API Documentation

// L:4659
jQuery.fn.extend({
	find: function( selector ) {
		//# http://api.jquery.com/pushStack/
		//# DOMエレメントの配列をjQueryオブジェクト化するメソッドのようだ。
		//# 第一引数はエレメントの配列を要求しているがなぜか""が渡されている
		//# 第二引数はjQueryオブジェクトを生成したメソッド名
		//# 第三引数はjQueryオブジェクトを生成したメソッドに渡された引数
		//# ドキュメントによれば、"for serialization"のためだと。
		//# pushStackの中身は後ほど読むが、とりあえずretに「要素ゼロのDOMエレメントコレクションを表すjQueryオブジェクト」が格納された
		var ret = this.pushStack( "", "find", selector ),
			length = 0;

		//# このループではthis.lengthを参照している。
		//# 今回のthisは$(document)なので、length==1である。
		//# thisが含む各エレメントをルートとして、
		//# 指定したselectorでエレメントを検索する
		for ( var i = 0, l = this.length; i < l; i++ ) {
			length = ret.length;
			//# thisが含むi番目の要素に対して検索をかける
			//# 詳細についてはあとでよむ。
			//# 検索の結果はretに追加される。
			jQuery.find( selector, this[i], ret );

			//# thisが複数の要素を含む場合、結果に同じエレメントが重複して含まれるかもしれない。
			//# ので、取り除く。
			if ( i > 0 ) {
				// Make sure that the results are unique
				for ( var n = length; n < ret.length; n++ ) {
					for ( var r = 0; r < length; r++ ) {
						if ( ret[r] === ret[n] ) {
							ret.splice(n--, 1);
							break;
						}
					}
				}
			}
		}

		//# で、結果を返す
		return ret;
	},
	//# ...

このメソッドでは、

  • 空のコレクションを表すjQueryオブジェクトを作成(pushStack)
  • 対象となるエレメントごとに、selectorによる検索を行い(jQuery.find)、結果を追加してゆく
  • 結果をユニークにして返す

という処理をしているようだ。

さて、あとは実装の詳細。
まず、空のコレクションを作るために呼ばれたpushStack()を読んでみよう。
.pushStack() | jQuery API Documentation

// L:95
jQuery.fn = jQuery.prototype = {
	constructor: jQuery,

	// L:231
	// Take an array of elements and push it onto the stack
	// (returning the new matched element set)
	pushStack: function( elems, name, selector ) {
		//# arguments:
		//#   elems=""
		//#   name="find"
		//#   selector="a"

		// Build a new jQuery matched element set
		//# constructorはjQueryが入っているので、
		//# jQuery() と同義
		var ret = this.constructor();

		//# 短いので解説しないけど、isArrayはマジモンのArrayオブジェクトかどうか
		//# 判断するためのメソッド(L: 481)
		//# ここではretに対してelemsの内容を追加してる。
		if ( jQuery.isArray( elems ) ) {
			push.apply( ret, elems );
		} else {
			//# elemsは""だけど、たぶんこれは空のコレクションとみなされる
			//# mergeは配列編( http://d.hatena.ne.jp/gnarl/20110212/1297436781 )で読んだ。retにelemsの要素を破壊的に追加するメソッド。
			jQuery.merge( ret, elems );
		}


		//# 今回は触れませんけど、end()でpushStack()前の状態に戻すために
		//# prevObjectに現在のオブジェクトを格納してる。
		// Add the old object onto the stack (as a reference)
		ret.prevObject = this;

		//# これは不明。
		ret.context = this.context;

		//# selectorプロパティを設定する。
		//# あっハードコードされた例外的処理だ!!
		if ( name === "find" ) {
			//# pushStackがfindから呼ばれた場合(今まさにですね)、
			//# selectorを特別なやり方で構築する。
			//# 今回のケースだと、
			//# 一般的なルートの場合              this.selector + ".find(a)"
			//# となるところを、このルートの場合  this.selector + " a"
			//# となります。意味はちょっとよくわからぬ。
			ret.selector = this.selector + (this.selector ? " " : "") + selector;
		} else if ( name ) {
			ret.selector = this.selector + "." + name + "(" + selector + ")";
		}

		//# 何はともあれ、空のコレクションを表現したjQueryオブジェクトが構築されました。
		//# エレメントのコレクションの場合、一般的なコレクションよりプロパティが色々増えてますね。
		// Return the newly-formed element set
		return ret;
	},

はい(疲れた)。

最後に、jQuery.find()を軽く読みます。

//# L:4632
//# Sizzleに委譲してるだけですね
jQuery.find = Sizzle;

//# L:3283
var Sizzle = function( selector, context, results, seed ) {
	results = results || [];
	context = context || document;
	//# CSSセレクタによる探索を実現するためのとても長い処理。
	//# 今回の趣旨とは外れるので読みません!

	//# resultsには要素のないjQueryオブジェクトが入ります。
	//# 探索の過程で、
	//# results.push を使用して要素を追加しています
	//# jQueryのpushはL:307で定義されていますが、Array.prototype.pushそのものです。
	//# this[this.length]=elm;this.length++; みたいな処理をしていると思われる。

	//# というわけで、ここで帰るのは、検索にマッチした要素を追加されたjQueryオブジェクトです。
	return results;
};

まとめ

セレクタを指定してjQueryオブジェクトを構築する際の流れを概観しました。
おつかれさまでした。

参考書籍

JavaScript 第5版
JavaScript 第5版
posted with amazlet at 11.02.12
David Flanagan
オライリー・ジャパン
売り上げランキング: 8437


前回とまったく同じアサマシリンクがあるのは前回からコピペしてきたからで、まあ全然効果ないことが判明してるんですけど一応貼っておきます!!金くれ!!