1. 程式人生 > >jQuery選擇器探究:TAG選擇器和CLASS選擇器

jQuery選擇器探究:TAG選擇器和CLASS選擇器

(jquery1.6.1版本)假設有一個html文件中存在多個<h1>標籤元素,那麼當我們敲入$("h1")後在jQuery內部將會執行怎樣的邏輯呢?

分析jQuery建構函式我們同樣定位到find( selector )方法,這個方法是jQuery的例項方法,程式碼如下:(5137行)
	find: function( selector ) {
		var self = this,
			i, l;

		if ( typeof selector !== "string" ) {
			return jQuery( selector ).filter(function() {
				for ( i = 0, l = self.length; i < l; i++ ) {
					if ( jQuery.contains( self[ i ], this ) ) {
						return true;
					}
				}
			});
		}

		var ret = this.pushStack( "", "find", selector ),
			length, n, r;

		for ( i = 0, l = this.length; i < l; i++ ) {
			length = ret.length;
			jQuery.find( selector, this[i], ret );

			if ( i > 0 ) {
				// Make sure that the results are unique
				for ( n = length; n < ret.length; n++ ) {
					for ( r = 0; r < length; r++ ) {
						if ( ret[r] === ret[n] ) {
							ret.splice(n--, 1);
							break;
						}
					}
				}
			}
		}

		return ret;
	},
我們暫不考慮其他呼叫的場景,只專注於selector引數為string型別時的邏輯。整體上呼叫流程是遵循jQuery一致的風格:例項方法"find(selector)"呼叫類方法"jQuery.find( selector, this[i], ret );",類方法find直接指向Sizzle建構函式"jQuery.find = Sizzle;"。當然還有一些其他的邏輯,如返回物件的構造、去重過濾等。

這時需要定位到Sizzle的建構函式,這時問題來了,Sizzle的定義不止一處,一處(3706行)定義是通用的,也是傳統的,邏輯較為複雜,一處(4807行)定義在document.querySelectorAll存在時過載並複用傳統定義。本文分析的是過載的相對簡單的Sizzle定義。
		Sizzle = function( query, context, extra, seed ) {
			context = context || document;

			// Only use querySelectorAll on non-XML documents
			// (ID selectors don't work in non-HTML documents)
			if ( !seed && !Sizzle.isXML(context) ) {
				// See if we find a selector to speed up
				var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
				
				if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
					// Speed-up: Sizzle("TAG")
					if ( match[1] ) {
						return makeArray( context.getElementsByTagName( query ), extra );
					
					// Speed-up: Sizzle(".CLASS")
					} else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
						return makeArray( context.getElementsByClassName( match[2] ), extra );
					}
				}
				
				if ( context.nodeType === 9 ) {
					// Speed-up: Sizzle("body")
					// The body element only exists once, optimize finding it
					if ( query === "body" && context.body ) {
						return makeArray( [ context.body ], extra );
						
					// Speed-up: Sizzle("#ID")
					} else if ( match && match[3] ) {
						var elem = context.getElementById( match[3] );

						// Check parentNode to catch when Blackberry 4.6 returns
						// nodes that are no longer in the document #6963
						if ( elem && elem.parentNode ) {
							// Handle the case where IE and Opera return items
							// by name instead of ID
							if ( elem.id === match[3] ) {
								return makeArray( [ elem ], extra );
							}
							
						} else {
							return makeArray( [], extra );
						}
					}
					
					try {
						return makeArray( context.querySelectorAll(query), extra );
					} catch(qsaError) {}

				// qSA works strangely on Element-rooted queries
				// We can work around this by specifying an extra ID on the root
				// and working up from there (Thanks to Andrew Dupont for the technique)
				// IE 8 doesn't work on object elements
				} else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
					var oldContext = context,
						old = context.getAttribute( "id" ),
						nid = old || id,
						hasParent = context.parentNode,
						relativeHierarchySelector = /^\s*[+~]/.test( query );

					if ( !old ) {
						context.setAttribute( "id", nid );
					} else {
						nid = nid.replace( /'/g, "\\$&" );
					}
					if ( relativeHierarchySelector && hasParent ) {
						context = context.parentNode;
					}

					try {
						if ( !relativeHierarchySelector || hasParent ) {
							return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
						}

					} catch(pseudoError) {
					} finally {
						if ( !old ) {
							oldContext.removeAttribute( "id" );
						}
					}
				}
			}
		
			return oldSizzle(query, context, extra, seed);
		};
在傳遞給Sizzle函式的四個引數( query, context, extra, seed )中,當context引數和seed引數滿足條件"if ( !seed && !Sizzle.isXML(context) )"時將執行過載的Sizzle邏輯,否則執行傳統Sizzle邏輯。這裡同樣用一個正則表示式物件"/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/"對選擇符引數query執行匹配,這個正則物件有三個分組,分別捕獲tag/class/id,也就是說過載的Sizzle函式功能上支援這三種最基本的選擇器(儘管實際中#id選擇符在jQuery建構函式中已經處理過了,理論上走不到這個地方來)。

當分組一存在時,執行tag選擇器邏輯;當分組二存在時,執行class選擇器邏輯。可以看到,對於這兩種選擇符,背後的邏輯回到原生dom的基本api上了:context.getElementsByTagName( query )、context.getElementsByClassName( match[2] ),當然,就像id選擇符一樣,Sizzle返回的物件集合肯定也不會是原始dom元素集合,所以makeArray函式的功能就是把選擇出來的原生dom元素集合inject到jQuery物件extra(第三個引數)中去:(4561行)
var makeArray = function( array, results ) {
	array = Array.prototype.slice.call( array, 0 );

	if ( results ) {
		results.push.apply( results, array );
		return results;
	}
	
	return array;
};
makeArray函式的邏輯比較簡單:接受兩個引數,第一個是待處理array like物件,第二個是待擴充套件物件,不傳遞第二個物件時,直接將array like物件轉換為真正的array物件,否則將轉換後的真正的array物件inject到第二個引數jQuery物件中去。

簡單的單純的TAG選擇器和CLASS選擇器執行邏輯就是這樣的,相對而言還是比較簡單的,後續分析複雜的組合的選擇器執行邏輯,也即真正的Sizzle核心。