jQuery源码学习(6)-Sizzle选择器(2)

1、CSS选择器的位置关系:

四种关系:"+" 紧挨着的兄弟关系;">" 父子关系;" " 祖先关系;"~" 之后的所有兄弟关系  

<div id="grandfather">
  <div id="father">
    <div id="child1"></div>
    <div id="child2"></div>
    <div id="child3"></div>
  </div>
</div>

grandfather与child1、2、3为祖先关系,用空格“ ”表示;

child1、child2为紧挨着的兄弟关系,用“+”表示;

grandfather与father、father与child1、2、3均为父子关系,用“>”表示;

child1与child3为普通兄弟关系,用“~”表示。

源码在Expr.relative里边定义了这些关系:

                //定义的元素关系:
		/*
		四种关系:"+" 紧挨着的兄弟关系;">" 父子关系;" " 祖先关系;"~" 之后的所有兄弟关系;
		first属性,用来标识两个节点的“紧密”程度,例如父子关系和临近兄弟关系就是紧密的。
		在创建位置匹配器时,会根据first属性来匹配合适的节点。
		*/
		relative: {
			">": {
				dir: "parentNode",
				first: true
			},
			" ": {
				dir: "parentNode"
			},
			"+": {
				dir: "previousSibling",
				first: true
			},
			"~": {
				dir: "previousSibling"
			}
		},

2、CSS浏览器实现的基本接口

除去querySelector,querySelectorAll

HTML文档一共有这么四个API:

  • getElementById,上下文只能是HTML文档。
  • getElementsByName,上下文只能是HTML文档。
  • getElementsByTagName,上下文可以是HTML文档,XML文档及元素节点。
  • getElementsByClassName,上下文可以是HTML文档及元素节点。IE8还没有支持。

所以要兼容的话sizzle最终只会有三种完全靠谱的可用(用于select函数中):

Expr.find = {
      'ID'    : context.getElementById,
      'CLASS' : context.getElementsByClassName,
      'TAG'   : context.getElementsByTagName
}

3、选择符匹配的方式。

通过select函数实现,它会调用上一节解释过的tokenize函数对选择器字符串进行词法分析。

源码解析如下:

        //引擎的主要入口函数
	/*
	 * select方法是Sizzle选择器包的核心方法之一,其主要完成下列任务:
	 * 1、调用tokenize方法完成对选择器的解析
	 * 2、对于没有初始集合(即seed没有赋值)且是单一块选择器(即选择器字符串中没有逗号),
	 *  完成下列事项:
	 *  1) 对于首选择器是ID类型且context是document的,则直接获取对象替代传入的context对象
	 *  2) 若选择器是单一选择器,且是id、class、tag类型的,则直接获取并返回匹配的DOM元素
	 *  3) 获取最后一个id、class、tag类型选择器的匹配DOM元素赋值给初始集合(即seed变量)
	 * 3、通过调用compile方法获取“预编译”代码并执行,获取并返回匹配的DOM元素
	 * 
	 * @param selector 已去掉头尾空白的选择器字符串
	 * @param context 执行匹配的最初的上下文(即DOM元素集合)。若context没有赋值,则取document。
	 * @param results 已匹配出的部分最终结果。若results没有赋值,则赋予空数组。
	 * @param seed 初始集合,搜索器搜到的符合条件的标签存在这里
	 */
	function select(selector, context, results, seed) {
		var i, tokens, token, type, find,
			//解析出词法格式
			match = tokenize(selector);

		if (!seed) { //如果外界没有指定初始集合seed了。
			// Try to minimize operations if there is only one group
			// 没有多组的情况下
			// 如果只是单个选择器的情况,也即是没有逗号的情况:div, p,可以特殊优化一下
			if (match.length === 1) {

				// Take a shortcut and set the context if the root selector is an ID
				tokens = match[0] = match[0].slice(0); //取出选择器Token序列

				//如果第一个是selector是id我们可以设置context快速查找
				/*
				 * 若选择器是以id类型开始,且第二个是关系符(即+~>或空格),
				 * 则获取id所属对象作为context继续完成后续的匹配
				 * 
				 * 此处的条件判断依次为:
				 * tokens.length > 2 :若tokens有两个以上的选择器
				 * (token = tokens[0]).type === "ID" :第一个选择器的类型为ID(即以#开头的),
				 * support.getById :支持getElementById函数
				 * context.nodeType === 9 :context对象是document
				 * documentIsHTML :当前处理的是HTML代码
				 * Expr.relative[tokens[1].type] :第二个tokens元素是一个关系(即+~>或空格)
				 * 在满足上面所有条件的情况下,执行if内的语句体
				 */
				if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
					support.getById && context.nodeType === 9 && documentIsHTML &&
					Expr.relative[tokens[1].type]) {
					// 将当前上下文指向第一个ID选择器指定的节点对象
				context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
					// 若当前上下文内没有指定ID对象,则直接返回results
					if (!context) {
						//如果context这个元素(selector第一个id选择器)都不存在就不用查找了
						return results;
					}
					//选择器字符串去掉第一个id选择器
					selector = selector.slice(tokens.shift().value.length);
				}

				// Fetch a seed set for right-to-left matching
				/* 
				 * 下面while循环的作用是用来根据最后一个id、class、tag类型的选择器获取初始集合
				 * 举个简单例子:若选择器是"div[title='2']",
				 * 代码根据div获取出所有的context下的div节点,并把这个集合赋给seed变量,
				 * 然后在调用compile函数,产生预编译代码,
				 * 预编译代码完成在上述初始集合中执行[title='2']的匹配
				 * 
				 * 首先,检查选择器字符串中是否存在与needsContext正则表达式相匹配的字符
				 * 若没有,则将依据选择器从右到左过滤DOM节点
				 * 否则,将先生成预编译代码后执行(调用compile方法)。 
				 */
				 
				/*
				 * "needsContext" : new RegExp("^" + whitespace
				 *      + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("
				 *      + whitespace + "*((?:-\\d)?\\d*)" + whitespace
				 *      + "*\\)|)(?=[^-]|$)", "i")
				 * needsContext用来匹配选择器字符串中是否包含下列内容:
				 * 1、>+~三种关系符
				 * 2、:even、:odd、:eq、:gt、:lt、:nth、:first、:last八种伪类
				 * 其中,(?=[^-]|$)用来过滤掉类似于:first-child等带中杠的且以上述八个单词开头的其它选择器
				 */
				 //检测是否有位置选择器,让i决定查找方向
				i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;

				//从右向左边查询
				while (i--) { //从后开始向前找!
					token = tokens[i]; //找到后边的规则

					// Abort if we hit a combinator
					// 如果遇到了关系选择器中止
					//
					//  > + ~ 空
					//
					if (Expr.relative[(type = token.type)]) {
						break;
					}

					/*
		                      先看看有没有搜索器find,搜索器就是浏览器一些原生的取DOM接口,简单的表述就是以下对象了
		                      Expr.find = {
					'ID'    : context.getElementById,
					'CLASS' : context.getElementsByClassName,
					'NAME'  : context.getElementsByName,
					'TAG'   : context.getElementsByTagName
		                      }
		                       */
					//如果是:first-child这类伪类就没有对应的搜索器了,此时会向前提取前一条规则token
					if ((find = Expr.find[type])) {

						// Search, expanding context for leading sibling combinators
						// 尝试一下能否通过这个搜索器搜到符合条件的初始集合seed
						/*
						 * rsibling = new RegExp(whitespace + "*[+~]")
						 * rsibling用于判定token选择器是否是兄弟关系符
						 */
						if ((seed = find(
							token.matches[0].replace(runescape, funescape),
							rsibling.test(tokens[0].type) && context.parentNode || context
						))) {

							//如果真的搜到了
							// If seed is empty or no tokens remain, we can return early
							//把最后一条规则去除掉
							tokens.splice(i, 1);
							selector = seed.length && toSelector(tokens);

							//看看当前剩余的选择器是否为空
							/*
							 * 若selector为空,说明选择器仅为单一id、class、tag类型的,
							 * 故直接返回获取的结果,否则,在获取seed的基础上继续匹配
							 */
							if (!selector) {
								//是的话,提前返回结果了。
								push.apply(results, seed);
								return results;
							}

							//已经找到了符合条件的seed集合,此时前边还有其他规则,跳出去
							break;
						}
					}
				}
			}
		}
		/*
		selector:"div > p + div.aaron input[type="checkbox"]"

		解析规则:
		1 按照从右到左
		2 取出最后一个token  比如[type="checkbox"]
		{
			matches : Array[3]
		        type    : "ATTR"
			value   : "[type="
		        checkbox "]"
		}
	    3 过滤类型 如果type是 > + ~ 空 四种关系选择器中的一种,则跳过,在继续过滤
	    4 直到匹配到为 ID,CLASS,NAME,TAG  中一种 , 因为这样才能通过浏览器的接口索取
	    5 此时seed种子合集中就有值了,这样把刷选的条件给缩的很小了
	    6 如果匹配的seed的合集有多个就需要进一步的过滤了,修正选择器 selector: "div > p + div.aaron [type="checkbox"]"
	    7 OK,跳到一下阶段的编译函数
	 */

		// "div > p + div.aaron [type="checkbox"]"

		// Compile and execute a filtering function
		// Provide `match` to avoid retokenization if we modified the selector above
		// 交由compile来生成一个称为终极匹配器
		// 通过这个匹配器过滤seed,把符合条件的结果放到results里边
		//
		//	//生成编译函数
		//  var superMatcher =   compile( selector, match )
		//
		//  //执行
		//	superMatcher(seed,context,!documentIsHTML,results,rsibling.test( selector ))
		//
		compile(selector, match)(
			seed,
			context, !documentIsHTML,
			results,
			rsibling.test(selector)
		);
		return results;
	}

4、select函数内部的编译函数机制

        对于一个 selector,我们把它生成 tokens,进行优化,优化的步骤包括去头和生成 seed 集合。对于这些种子集合,我们知道最后的匹配结果是来自于集合中的一部分,似乎接下来的任务也已经明确:对种子进行过滤(或者称其为匹配)。匹配的过程其实很简单,就是对 DOM 元素进行判断,而且若是那种一代关系(>)或临近兄弟关系(+),不满足,就结束,若为后代关系(“ ” )或者兄弟关系(~),会进行多次判断,要么找到一个正确的,要么结束,不过仍需要考虑回溯问题。比如div > div.seq h2 ~ p,已经对应的把它们划分成 tokens,如果每个 seed 都走一遍流程显然太麻烦。一种比较合理的方法就是对应每个可判断的 token 生成一个闭包函数,统一进行查找。

Expr.filter 是用来生成匹配函数的:

Expr.filter = {
  "ID": function(id){...},
  "TAG": function(nodeNameSelector){...},
  "CLASS": function(className){...},
  "ATTR": function(name, operator, check){...},
  "CHILD": function(type, what, argument, first, last){...},
  "PSEUDO": function(pseudo, argument){...}
}

Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行,而是先根据规则组合出一个大的匹配方法,最后一步执行。这个匹配方法就是编译函数compile();这样子对于 seed 中的每一个元素,就可以用这个编译函数对其父元素或兄弟节点挨个判断,效率大大提升,即所谓的编译一次,多次使用。

compile()函数源码:

        //编译函数机制
	//通过传递进来的selector和match生成匹配器:
	compile = Sizzle.compile = function(selector, group /* Internal Use Only */ ) {
		var i,
			setMatchers = [],
			elementMatchers = [],
			cached = compilerCache[selector + " "];

		if (!cached) { //依旧看看有没有缓存
			// Generate a function of recursive functions that can be used to check each element
			if (!group) {
				//如果没有词法解析过
				group = tokenize(selector);
			}

			i = group.length; //从后开始生成匹配器

			//如果是有并联选择器这里多次等循环
			while (i--) {
				//这里用matcherFromTokens来生成对应Token的匹配器
				cached = matcherFromTokens(group[i]);
				if (cached[expando]) {   
		//如果选择器中有伪类的选择器压入setMatchers,
		//cached[expando]在生成匹配器函数的时候就判断是否有伪类而赋值了
					setMatchers.push(cached);
				} else { 
				//普通的那些匹配器都压入了elementMatchers里边
				elementMatchers.push(cached);
			}
		}
		// Cache the compiled function
		// 这里可以看到,是通过matcherFromGroupMatchers这个函数来生成最终的匹配器
		// compilerCache缓存编译函数
		//matcherFromGroupMatchers函数返回一个执行所有的匹配器最终将匹配成功的集合
		//保存在作为参数传递过来的数组对象results中的curry化函数
		cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));
	}
	//把这个终极匹配器返回到select函数中
	return cached;
};

其中调用了matcherFromTokens()函数来生成对应的Token匹配器。

//生成用于匹配单个选择器组的函数
	//充当了selector“tokens”与Expr中定义的匹配方法的串联与纽带的作用,
	//可以说选择符的各种排列组合都是能适应的了
	//Sizzle巧妙的就是它没有直接将拿到的“分词”结果与Expr中的方法逐个匹配逐个执行,
	//而是先根据规则组合出一个大的匹配方法,最后一步执行。但是组合之后怎么执行的
	function matcherFromTokens(tokens) {
		var checkContext, matcher, j,
			len = tokens.length,
			leadingRelative = Expr.relative[tokens[0].type],
			implicitRelative = leadingRelative || Expr.relative[" "], //亲密度关系
			i = leadingRelative ? 1 : 0,

			// The foundational matcher ensures that elements are reachable from top-level context(s)
			// 确保这些元素可以在context中找到
			// 确保元素都能找到
			// addCombinator 就是对 Expr.relative 进行判断
			/*
			  Expr.relative = {
				">": { dir: "parentNode", first: true },
				" ": { dir: "parentNode" },
				"+": { dir: "previousSibling", first: true },
				"~": { dir: "previousSibling" }
			  };
			 */
			matchContext = addCombinator(function(elem) {
				return elem === checkContext;
			}, implicitRelative, true),

			matchAnyContext = addCombinator(function(elem) {
				return indexOf.call(checkContext, elem) > -1;
			}, implicitRelative, true),

			//这里用来确定元素在哪个context
			matchers = [
				function(elem, context, xml) {
					return (!leadingRelative && (xml || context !== outermostContext)) || (
						(checkContext = context).nodeType ?
						matchContext(elem, context, xml) :
						matchAnyContext(elem, context, xml));
				}
			];

		for (; i < len; i++) {
			// Expr.relative 匹配关系选择器类型
			// "空 > ~ +"
			if ((matcher = Expr.relative[tokens[i].type])) {
				//当遇到关系选择器时elementMatcher函数将matchers数组中的函数生成一个函数
				//(elementMatcher利用了闭包所以matchers一直存在内存中)
				matchers = [addCombinator(elementMatcher(matchers), matcher)];
			} else {
				//过滤  ATTR CHILD CLASS ID PSEUDO TAG
				matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches);

				// Return special upon seeing a positional matcher
				//返回一个特殊的位置匹配函数
				//伪类会把selector分两部分
				if (matcher[expando]) {
					// Find the next relative operator (if any) for proper handling
					// 发现下一个关系操作符(如果有话)并做适当处理
					j = ++i;
					for (; j < len; j++) {
						if (Expr.relative[tokens[j].type]) { //如果位置伪类后面还有关系选择器还需要筛选
							break;
						}
					}
					return setMatcher(
						i > 1 && elementMatcher(matchers),
						i > 1 && toSelector(
							// If the preceding token was a descendant combinator, 
                                                        //insert an implicit any-element `*`
							tokens.slice(0, i - 1).concat({
								value: tokens[i - 2].type === " " ? "*" : ""
							})
						).replace(rtrim, "$1"),
						matcher,
						 //如果位置伪类后面还有选择器需要筛选
						i < j && matcherFromTokens(tokens.slice(i, j)),
						//如果位置伪类后面还有关系选择器还需要筛选
						j < len && matcherFromTokens((tokens = tokens.slice(j))), 
						j < len && toSelector(tokens)
					);
				}
				matchers.push(matcher);
			}
		}
		return elementMatcher(matchers);
	}

通过matcherFromGroupMatchers()函数来生成最终的匹配器(可能有多个,如果有逗号的话)。

        //返回的是一个终极匹配器superMatcher
	//生成用于匹配单个选择器群组的函数
	function matcherFromGroupMatchers(elementMatchers, setMatchers) {
		// A counter to specify which element is currently being matched
		// 用计数器来指定当前哪个元素正在匹配
		var matcherCachedRuns = 0,
			bySet = setMatchers.length > 0,
			byElement = elementMatchers.length > 0,

			superMatcher = function(seed, context, xml, results, expandContext) {
				var elem, j, matcher,
					setMatched = [],
					matchedCount = 0,
					i = "0",
					unmatched = seed && [],
					outermost = expandContext != null,
					contextBackup = outermostContext,

					//这一步很关键!
					//如果说有初始集合seed,那用它
					//如果没有,那只能把整个DOM树节点取出来过滤了,可以看出选择器最右边应该写一个
					// We must always have either seed elements or context
				elems = seed || byElement && Expr.find["TAG"]("*", expandContext && context.parentNode || context),
					// Use integer dirruns iff this is the outermost matcher
					dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
					len = elems.length;

				if (outermost) {
					outermostContext = context !== document && context;
					cachedruns = matcherCachedRuns;
				}

				//好,开始过滤这个elems集合了!
				// Add elements passing elementMatchers directly to results
				// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
				// Support: IE<9, Safari
				// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
				for (; i !== len && (elem = elems[i]) != null; i++) {
					if (byElement && elem) {
						j = 0;

						//把所有的过滤器拿出来
						//这里的每个匹配器都是上边提到的终极匹配器,既然是终极匹配器,那为什么会有多个呢?
						//因为:div, p实际上是需要两个终极匹配器的(逗号分隔开表示有两个选择器了),:)
						while ((matcher = elementMatchers[j++])) {
							//过滤这个集合的元素elem,如果符合匹配器的规则,那么就添加到结果集里边去
							if (matcher(elem, context, xml)) {
								results.push(elem);
								break;
							}
						}
						if (outermost) {
							dirruns = dirrunsUnique;
							cachedruns = ++matcherCachedRuns;
						}
					}

					// Track unmatched elements for set filters
					if (bySet) {
						// They will have gone through all possible matchers
						if ((elem = !matcher && elem)) {
							matchedCount--;
						}

						// Lengthen the array for every element, matched or not
						if (seed) {
							unmatched.push(elem);
						}
					}
				}

				// Apply set filters to unmatched elements
				matchedCount += i;
				if (bySet && i !== matchedCount) {
					j = 0;
					while ((matcher = setMatchers[j++])) {
						matcher(unmatched, setMatched, context, xml);
					}

					if (seed) {
						// Reintegrate element matches to eliminate the need for sorting
						if (matchedCount > 0) {
							while (i--) {
								if (!(unmatched[i] || setMatched[i])) {
									setMatched[i] = pop.call(results);
								}
							}
						}

						// Discard index placeholder values to get only actual matches
						setMatched = condense(setMatched);
					}

					// Add matches to results
					push.apply(results, setMatched);

					// Seedless set matches succeeding multiple successful matchers stipulate sorting
					if (outermost && !seed && setMatched.length > 0 &&
						(matchedCount + setMatchers.length) > 1) {

						Sizzle.uniqueSort(results);
					}
				}

				// Override manipulation of globals by nested matchers
				if (outermost) {
					dirruns = dirrunsUnique;
					outermostContext = contextBackup;
				}

				return unmatched;
			};
		return bySet ?
			markFunction(superMatcher) :
			superMatcher;
	}

matcherFromTokens()函数是通过elementMatchers()函数来生成单个的终极匹配器的:

function elementMatcher(matchers) {
		//生成一个终极匹配器
		return matchers.length > 1 ?
		//如果是多个匹配器的情况,那么就需要elem符合全部匹配器规则
			function(elem, context, xml) {
				var i = matchers.length;
				//从右到左开始匹配
				while (i--) {
					//如果有一个没匹配中,那就说明该节点elem不符合规则
					if (!matchers[i](elem, context, xml)) {
						return false;
					}
				}
				return true;
		} :
		//单个匹配器的话就返回自己即可
			matchers[0];
	}

matcherFromTokens()函数通过addCombinator()函数将关系选择符合并分组:

//matcher为当前词素前的“终极匹配器”
	//combinator为位置词素
	//根据关系选择器检查
	//如果是这类没有位置词素的选择器:’#id.clr[name="checkbox"]‘,
	//从右到左依次看看当前节点elem是否匹配规则即可。但是由于有了位置词素,
	//那么判断的时候就不是简单判断当前节点了,
	//可能需要判断elem的兄弟或者父亲节点是否依次符合规则。
	//这是一个递归深搜的过程。
	//addCombinator方法就是为了生成有位置词素的匹配器。
	function addCombinator(matcher, combinator, base) {
		var dir = combinator.dir,
			checkNonElements = base && dir === "parentNode",
			doneName = done++; //第几个关系选择器

		return combinator.first ?
		// Check against closest ancestor/preceding element
		// 检查最靠近的祖先元素
		// 如果是紧密关系的位置词素
		function(elem, context, xml) {
			while ((elem = elem[dir])) {
				if (elem.nodeType === 1 || checkNonElements) {
					//找到第一个亲密的节点,立马就用终极匹配器判断这个节点是否符合前面的规则
					return matcher(elem, context, xml);
				}
			}
		} :

		// Check against all ancestor/preceding elements
		//检查最靠近的祖先元素或兄弟元素(概据>、~、+还有空格检查)
		//如果是不紧密关系的位置词素
		function(elem, context, xml) {
			var data, cache, outerCache,
				dirkey = dirruns + " " + doneName;

			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
			// 我们不可以在xml节点上设置任意数据,所以它们不会从dir缓存中受益
			if (xml) {
				while ((elem = elem[dir])) {
					if (elem.nodeType === 1 || checkNonElements) {
						if (matcher(elem, context, xml)) {
							return true;
						}
					}
				}
			} else {
				while ((elem = elem[dir])) {
		            //如果是不紧密的位置关系
		            //那么一直匹配到true为止
		            //例如祖宗关系的话,就一直找父亲节点直到有一个祖先节点符合规则为止
					if (elem.nodeType === 1 || checkNonElements) {
						outerCache = elem[expando] || (elem[expando] = {});
						//如果有缓存且符合下列条件则不用再次调用matcher函数
						if ((cache = outerCache[dir]) && cache[0] === dirkey) {
							if ((data = cache[1]) === true || data === cachedruns) {
								return data === true;
							}
						} else {
							cache = outerCache[dir] = [dirkey];
							cache[1] = matcher(elem, context, xml) || cachedruns; 
                                                        //cachedruns//正在匹配第几个元素
							if (cache[1] === true) {
								return true;
							}
						}
					}
				}
			}
		};
	}

整个编译函数返回的结果是一个根据关系选择器分组后再组合的嵌套很深的闭包函数了。

5、流程总结:

Sizzle 虽然独立出去,单独成一个项目,不过在 jQuery 中的代表就是 jQuery.find 函数,这两个函数其实就是同一个,完全等价的。然后介绍 tokensize 函数,这个函数的被称为词法分析,作用就是将 selector 划分成 tokens 数组,数组每个元素都有 value 和 type 值。然后是 select 函数,这个函数的功能起着优化作用,去头去尾,并 Expr.find 函数生成 seed 种子数组。compile 函数进行预编译,就是对去掉 seed 后剩下的 selector 生成闭包函数,又把闭包函数生成一个大的 superMatcher 函数,这个时候就可用这个 superMatcher(seed) 来处理 seed 并得到最终的结果。superMatcher()函数并不是直接定义的函数,通过matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出来的一个curry化的函数,但是最后执行起重要作用的是它。

流程图:

第一步

div > p + div.aaron input[type="checkbox"]

从最右边先通过 Expr.find 获得 seed 数组,在这里的 input 是 TAG,所以通过 getElementsByTagName() 函数。

第二步

重组 selector,此时除去 input 之后的 selector:

div > p + div.aaron [type="checkbox"]

第三步

此时通过 Expr.relative 将 tokens 根据关系分成紧密关系和非紧密关系,比如 [“>”, “+”] 就是紧密关系,其 first = true。而对于 [” “, “~”] 就是非紧密关系。紧密关系在筛选时可以快速判断。

matcherFromTokens 根据关系编译闭包函数,为四组:

div > 
p + 
div.aaron 
input[type="checkbox"]

编译函数主要借助 Expr.filter 和 Expr.relative。

A: 抽出div元素, 对应的是TAG类型
B: 通过Expr.filter找到对应匹配的处理器,返回一个闭包处理器

如:TAG方法

C:将返回的curry方法放入到matchers匹配器组中,继续分解

D:抽出子元素选择器 '>' ,对应的类型 type: ">" 

E:通过Expr.relative找到elementMatcher方法分组合并多个词素的的编译函数。所以这里其实就是执行了各自Expr.filter匹配中的的判断方法了,matcher方法原来运行的结果都是bool值,所以这里只返回了一个组合闭包,通过这个筛选闭包,各自处理自己内部的元素。

F:返回的这个匹配器还是不够的,因为没有规范搜索范围的优先级,所以这时候还要引入addCombinator方法

addCombinator方法源码:

//matcher为当前词素前的“终极匹配器”
	//combinator为位置词素
	//根据关系选择器检查
	//如果是这类没有位置词素的选择器:’#id.clr[name="checkbox"]‘,
	//从右到左依次看看当前节点elem是否匹配规则即可。但是由于有了位置词素,
	//那么判断的时候就不是简单判断当前节点了,
	//可能需要判断elem的兄弟或者父亲节点是否依次符合规则。
	//这是一个递归深搜的过程。
	//addCombinator方法就是为了生成有位置词素的匹配器。
	function addCombinator(matcher, combinator, base) {
		var dir = combinator.dir,
			checkNonElements = base && dir === "parentNode",
			doneName = done++; //第几个关系选择器

		return combinator.first ?
		// Check against closest ancestor/preceding element
		// 检查最靠近的祖先元素
		// 如果是紧密关系的位置词素
		function(elem, context, xml) {
			while ((elem = elem[dir])) {
				if (elem.nodeType === 1 || checkNonElements) {
					//找到第一个亲密的节点,立马就用终极匹配器判断这个节点是否符合前面的规则
					return matcher(elem, context, xml);
				}
			}
		} :

		// Check against all ancestor/preceding elements
		//检查最靠近的祖先元素或兄弟元素(概据>、~、+还有空格检查)
		//如果是不紧密关系的位置词素
		function(elem, context, xml) {
			var data, cache, outerCache,
				dirkey = dirruns + " " + doneName;

			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
			// 我们不可以在xml节点上设置任意数据,所以它们不会从dir缓存中受益
			if (xml) {
				while ((elem = elem[dir])) {
					if (elem.nodeType === 1 || checkNonElements) {
						if (matcher(elem, context, xml)) {
							return true;
						}
					}
				}
			} else {
				while ((elem = elem[dir])) {
		            //如果是不紧密的位置关系
		            //那么一直匹配到true为止
		            //例如祖宗关系的话,就一直找父亲节点直到有一个祖先节点符合规则为止
					if (elem.nodeType === 1 || checkNonElements) {
						outerCache = elem[expando] || (elem[expando] = {});
						//如果有缓存且符合下列条件则不用再次调用matcher函数
						if ((cache = outerCache[dir]) && cache[0] === dirkey) {
							if ((data = cache[1]) === true || data === cachedruns) {
								return data === true;
							}
						}else {
						cache = outerCache[dir] = [dirkey];
					  //cachedruns正在匹配第几个元素
						cache[1] = matcher(elem, context, xml) || cachedruns; 
						if (cache[1] === true) {
							return true;
						}
					}
				}
			}
		}
	};
} 

G:根据Expr.relative -> first:true 两个关系的“紧密”程度,如果是是亲密关系addCombinator返回

function( elem, context, xml ) {
    while ( (elem = elem[ dir ]) ) {
        if ( elem.nodeType === 1 || checkNonElements ) {
            return matcher( elem, context, xml );
        }
    }
}

所以可见如果是紧密关系的位置词素,找到第一个亲密的节点,立马就用终极匹配器判断这个节点是否符合前面的规则

这是第一组终极匹配器的生成流程了

可见过程极其复杂,被包装了三层

依次:

addCombinator
elementMatcher
Expr.relative

通过继续分解下一组,遇到关系选择器又继续依照以上的过程分解。但是有一个不同的地方,下一个分组会把上一个分组给一并合并了。所以整个关系就是一个依赖嵌套很深的结构。最终暴露出来的终极匹配器其实只有一个闭包,但是有内嵌很深的分组闭包了。依照从左边往右依次生成闭包,然后把上一组闭包又push到下一组闭包。

所以在最外层也就是

type=["checkbox"]

第四步

将所有的编译闭包函数放到一起,生成 superMatcher 函数。

function( elem, context, xml ) {
    var i = matchers.length;
    while ( i-- ) {
        if ( !matchers[i]( elem, context, xml ) ) {
            return false;
        }
    }
    return true;
}

从右向左,处理 seed 集合,如果有一个不匹配,则返回 false。如果成功匹配,则说明该 seed 元素是符合筛选条件的,返回给 results。

还有一个更细致的:由于版本不同,代码行数有差异,但是也相差不多。

(来源:https://blog.csdn.net/huantuo4908/article/details/70208476

总流程:


tokenize函数的处理流程:


compile函数的处理流程:


这个过程源码部分都还没仔细分析,先弄懂整个流程,这里觉得博客https://blog.csdn.net/WuLex/article/details/78487717的总结挺好的,起到把知识点串联的作用。

另外,关于位置伪类的匹配原理,几篇博客讲的都不甚详细,大致了解是在sizzle的matcherFromTokens函数中会有判断分支来处理它,利用setMatcher函数生成对应的匹配函数。后面再详细了解。

5、Sizzle高效原因

从原理上分析

  1. 浏览器原生支持的方法,效率肯定比Sizzle自己js写的方法要高,优先使用也能保证Sizzle更高的工作效率,在不支持querySelectorAll方法的情况下,Sizzle也是优先判断是不是可以直接使用getElementById、getElementsByTag、getElementsByClassName等方法解决问题。
  2. 相对复杂的情况,Sizzle总是选择先尽可能利用原生方法来查询选择来缩小待选范围,然后才会利用前面介绍的“编译原理”来对待选范围的元素逐个匹配筛选。进入到“编译”这个环节的工作流程有些复杂,效率相比前面的方法肯定会稍低一些,但Sizzle在努力尽量少用这些方法,同时也努力让给这些方法处理的结果集尽量小和简单,以便获得更高的效率。
  3. 即便进入到这个“编译”的流程,Sizzle还做了我们前面为了优先解释清楚流程而暂时忽略、没有介绍的缓存机制。Sizzle.compile是“编译”入口,也就是它会调用第三个核心方法superMatcher,compile方法将根据selector生成的匹配函数缓存起来了。还不止如此,tokenize方法,它其实也将根据selector做的分词结果缓存起来了。也就是说,当我们执行过一次Sizzle (selector)方法以后,下次再直接调用Sizzle (selector)方法,它内部最耗性能的“编译”过程不会再耗太多性能了,直接取之前缓存的方法就可以了。我在想所谓“编译”的最大好处之一可能也就是便于缓存,所谓“编译”在这里可能也就可以理解成是生成预处理的函数存储起来备用。

正确选择选择器

        正确使用选择器引擎对于提高页面性能起了至关重要的作用。使用合适的选择器表达式可以提高性能、增强语义并简化逻辑。在传统用法中,最常用的简单选择器包括ID选择器、Class选择器和类型标签选择器。其中ID选择器是速度最快的,这主要是因为它使用JavaScript的内置函数getElementById();其次是类型选择器,因为它使用JavaScript的内置函数getElementsByTag();速度最慢的是Class选择器,其需要通过解析 HTML文档树,并且需要在浏览器内核外递归,这种递归遍历是无法被优化的。

Class选择器在文档中使用频率靠前,这无疑会增加系统的负担,因为每使用一次Class选择器,整个文档就会被解析一遍,并遍历每个节点。

选择器性能优化建议:

1、多用ID选择器 , 总是从#id选择器来继承;

效率更高,那是因为$("#container")是不需要经过Sizzle选择器引擎处理的,jquery对仅含id选择器的处理方式是直接使用了浏览器的内置函数document.getElementById(),所以其效率是非常之高的。

2、少直接使用Class选择器,可以使用复合选择器,在class前面使用tag;

jQuery中第二快的选择器就是tag选择器(如$(‘head’)),因为它和直接来自于原生的Javascript方法getElementByTagName()。所以最好总是用tag来修饰class(并且不要忘了就近的ID)

jQuery中class选择器是最慢的,因为在IE浏览器下它会遍历所有的DOM节点。尽量避免使用class选择器。也不要用tag来修饰ID。

3、多用父子关系,少用嵌套关系;

例如,使用parent>child代替parent child。因为">"是child选择器,只从子节点里匹配,不递归。而" "是后代选择器,递归匹配所有子节点及子节点的子节点,即后代节点。

4、缓存jQuery对象。

如果选出结果不发生变化的话,不妨缓存jQuery对象,这样就可以提高系统性能。养成缓存jQuery对象的习惯可以让你在不经意间就能够完成主要的性能优化。

通过链式调用,采用find(),end(),children(),has,filter()等方法,来过滤结果集,减少$()查找方法调用,提升性能

跟jQuery选择器有关的性能问题是尽量采用链式调用来操作缓存选择器结果集因为每一个$()的调用都会导致一次新的查找,所以,采用链式调用和设置变量缓存结果集,减少查找,提升性能。

阅读更多
文章标签: Select
个人分类: jQuery源码分析
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭