天天看点

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核心。

继续阅读