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選擇器有關的性能問題是儘量採用鏈式調用來操作緩存選擇器結果集因爲每一個$()的調用都會導致一次新的查找,所以,採用鏈式調用和設置變量緩存結果集,減少查找,提升性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章