好了有了以前的詞法分析過程,如今咱們來到select函數來,這個函數的總體流程,前面也大概說過:node
1. 先作詞法分析得到token列表數組
2. 若是有種子集合直接到編譯過程緩存
3. 若是沒有種子集合而且是單組選擇符(沒有逗號)閉包
(1)嘗試縮小上下文:若是第一個token是ID選擇符,則會執行Expr.find["ID"]的方法來找到這個上下文,之後全部的查詢都是在這個上下文進行,而後把第一個ID選擇符剔除。app
(2)嘗試尋找種子集合:從右開始往左分析token,若是遇到關係選擇符(> + ~ 空)終止循環,不然經過Expr.find的方法嘗試尋找符合條件的DOM集合,若是找到了就講種子集合保存起來。dom
4. 進入到編譯過程ide
這裏面須要講解下爲什麼要進行篩選的工做,前面也說過,目的就是爲了儘可能縮小查詢範圍,首先縮小上下文範圍,而後縮小種子集合範圍,由於從右向左查詢的過程更快,因此咱們是從後面開始搜索種子集合,搜索到以後,後面全部的分析過程都是在這些種子集合基礎之上進行的。函數
Expr.find = {
'ID' : context.getElementById,
'CLASS' : context.getElementsByClassName,
'NAME' : context.getElementsByName,
'TAG' : context.getElementsByTagName
}性能
Expr.find返回一個函數,這個函數根據當前參數進行驗證,看看是不是指定類型的節點,能夠查驗ID,CLASS,NAME和TAG等等。咱們以class爲例:優化
Expr.find["CLASS"] = support.getElementsByClassName && function(className, context) { if (typeof context.getElementsByClassName !== strundefined && documentIsHTML) { return context.getElementsByClassName(className); } };
Expr.find["CLASS"]返回一個函數,這個函數有兩個參數,第一個參數className,第二個參數context,在select裏面就是經過這個函數來查詢指定className的DOM集合,找到之後就是seed種子集合。
select源碼以下:
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 // 單組選擇符而且以ID開頭,能夠作些優化 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序列 //找到context上下文 if (tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]) { context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0]; if (!context) { return results; } //去掉以查詢的選擇符 selector = selector.slice(tokens.shift().value.length); } // Fetch a seed set for right-to-left matching //僞類方面,暫不討論 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; } //對選擇符進行過濾查詢,看看可否找到符合條件的節點 if ((find = Expr.find[type])) { // Search, expanding context for leading sibling combinators 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); if (!selector) {//若是選擇符爲空,查詢完畢 push.apply(results, seed); return results; } break; } } } } } compile(selector, match)( seed, context, !documentIsHTML, results, rsibling.test(selector) ); return results; }
走到這裏咱們發現,咱們如今已經擁有了哪些信息:token列表,縮小的context和種子集合,那麼剩下的事情是否是對種子集合的每一個元素再和token列表一一校驗,留下符合條件的,刪除不符合條件的是否是查詢就完成了?
正常看起來是這樣的,咱們對每一個種子進行邊解析邊分析的過程符合要求,可是Sizzle作了更進一步的處理,經過空間換時間的方式,提升了查詢性能,他採用了一種叫先編譯後執行的過程。首先把全部的token元素生成一個嵌套的函數,而後再針對種子集合,去執行這個函數,把符合條件的留下來,因爲函數是經過閉包的方式來保存,因此當同一個選擇符查詢時,能夠直接執行函數來查詢,從而加快了查詢的性能,而不用每次從頭解析。
這個函數包括兩種狀況:
1. 關係選擇器:若是token是關係選擇器,則生成函數的時候須要同上一個選擇器共同生成。
2. 非關係選擇器:若是是非關係選擇器,則直接判斷種子是否知足條件便可。
好比 div > a 咱們生成函數1 父節點是不是div 函數2 自己是不是a標籤 函數1+函數2 就是咱們最終生成的Match匹配函數,對每一個種子進行執行Match匹配函數便可。
咱們看看compile的總體思路
1. 從緩存查詢是否已經編譯過,有的話直接拿出來
2. 判斷是否tokenize過,沒有的話,補一下
3. 對group的每一個元素進行matcherFromTokens方法,得到該token組的組合函數,若是是包含僞類,則添加到setMatchers數組,不然添加到elementMatchers數組
4. 最後對setMatchers和elementMatchers執行matcherFromGroupMatchers方法。
這裏要解釋下matcherFromTokens和matcherFromGroupMatchers方法,生成最終的包含非僞類和僞類的最終匹配函數:
matcherFromTokens: 將一組token數組轉換爲一個Match匹配函數,好比div > a 就生成一個包含兩個函數的Match匹配函數。
matcherFromGroupMatchers:因爲存在僞類和非僞類選擇符兩種狀況,這個函數的目的是融合這兩種狀況,最終生成一個超級匹配函數。
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--) { //得到Match匹配函數 cached = matcherFromTokens(group[i]); if (cached[expando]) {//包含僞類的添加到setMatchers數組 setMatchers.push(cached); } else { //不然添加到elementMatchers數組 elementMatchers.push(cached); } } // Cache the compiled function // 最終融合成一個超級匹配函數返回 cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers)); } // return cached; };
下面介紹下matcherFromTokens這個方法:輸入參數是tokens,而後對每一個token進行處理,這裏須要瞭解一個知識點:
非僞類的選擇符 有普通選擇符和關係選擇符兩種,關係選擇符包括如下幾種狀況:> 空格 + ~
保存在Expr.relative對象中
> : 表示是父子關係 對應DOM屬性parentNode 是元素的第一個節點因此 first爲true
空格:表示是後代關係 對應DOM屬性parentNode
+:表示附近兄弟關係 對應DOM屬性previousSibling 是元素的第一個節點因此 first爲true
~:表示普通兄弟關係 對應DOM屬性previousSibling
在matcherFromTokens方法中就會對非關係型和關係型分部處理:
matchers是存放各個選擇符過濾函數的數組
1. 非關係型運算符:把該類型的過濾函數拷貝一份push到matchers數組中便可,好比前面#div_test > span input[checked=true]中的 input span等等
2. 關係型運算符:把當前的關係選擇符和前面的選擇符一塊兒共同組成一個過濾函數,push到matchers數組中。
最後把matchers數組統一經過elementMatcher函數來生成一個最終的過濾函數
elementMatcher方法的做用是將一個函數數組,生成一個過濾函數,這個函數會遍歷執行各個函數
//將mathcers數組的全部方法合併成一個單獨的函數,這個函數會挨個執行數組中的方法。 function elementMatcher(matchers) { return matchers.length > 1 ? //若是是多個匹配器,循環判斷 function(elem, context, xml) { var i = matchers.length; //從右到左開始匹配 while (i--) { //若是有一個沒匹配中,返回false if (!matchers[i](elem, context, xml)) { return false; } } return true; } : //單個匹配器的話就返回本身便可 matchers[0]; }
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、 //若是first爲true表示是 > 或者 + 選擇符 取最近的一個元素便可,一次查詢 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 if (xml) { while ((elem = elem[dir])) { if (elem.nodeType === 1 || checkNonElements) { if (matcher(elem, context, xml)) { return true; } } } } else { while ((elem = elem[dir])) { //一直向上查找,直到找到一個爲止 if (elem.nodeType === 1 || checkNonElements) { outerCache = elem[expando] || (elem[expando] = {}); 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; } } } } } }; }
有了上面兩個函數的支持後,matcherFromTokens的做用就遍歷tokens數組
//將tokens數組轉換成一個過濾函數,略過僞類部分
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) matchContext = addCombinator(function(elem) { return elem === checkContext; }, implicitRelative, true), matchAnyContext = addCombinator(function(elem) { return indexOf.call(checkContext, elem) > -1; }, implicitRelative, true), 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++) { // 若是是關係型選擇符,執行addCombinator方法 if ((matcher = Expr.relative[tokens[i].type])) { matchers = [addCombinator(elementMatcher(matchers), matcher)]; } else { 不然直接找到選擇器的過濾函數 matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); matchers.push(matcher); } } return elementMatcher(matchers); }
下面咱們來看看Expr.filter,前面說過他總共有
ID:ID選擇符
Class:類選擇符
Tag:標籤選擇符
ATTR:屬性標籤
CHILD:包括(only|first|last|nth|nth-last)-(child|of-type)等等對子類的標籤
PSEUDO:其餘僞類選擇符
這幾種類型,那Expr.filter裏面包含的分別是各類類型的過濾函數,好比Expr.filter["ID"]
Expr.filter["ID"] = function(id) { var attrId = id.replace(runescape, funescape); return function(elem) { return elem.getAttribute("id") === attrId; }; };
這個函數的做用就是經過參數ID返回新的函數FUNC_ID,這個函數參數傳入一個DOM元素(其實就是以前的seed集合),判斷這個DOM元素的ID是不是指定ID,也就是判斷seed集合是不是選擇符指定的ID元素。
"TAG": function(nodeNameSelector) { var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase(); return nodeNameSelector === "*" ? function() { return true; } : function(elem) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; },
TAG的函數的做用就是經過參數nodeNameSelector生成一個新的函數FUNC_NODE,這個函數判斷傳入的DOM元素是不是指定的nodeNameSelector類型標籤。
總之就是一組過濾函數,判斷DOM節點是否符合選擇符的條件,知足就留下,不然剔除掉。
終於要對咱們的Seed進行過濾了!前面咱們經過matcherFromTokens方法生成了一個包含全部選擇符過濾函數的統一過濾函數,下面還須要對seed集合進行挨個過濾,就是matcherFromGroupMatchers要作的事情:
matcherFromGroupMatchers函數主要針對僞類和非僞類綜合處理,咱們暫不考慮僞類狀況matcherFromGroupMatchers能夠簡化許多:
能夠看到整個代碼最關鍵的地方就是有一個雙層循環,把全部的seed集合拿出來對全部的過濾函數進行執行,把返回true的集合保留下來,就是咱們最終要查詢的結果:
function matcherFromGroupMatchers(elementMatchers) { var matcherCachedRuns = 0, byElement = elementMatchers.length > 0, return function(seed, context, xml, results, expandContext) { var elem, j, matcher, setMatched = [], i = "0", unmatched = seed && [], outermost = expandContext != null, contextBackup = outermostContext, //能夠看到若是沒有seed集合就會把全部的DOM節點查詢出來當作seed (Expr.find["TAG"]("*")) 因此咱們在寫選擇符的時候最好不要在末尾寫* // 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; } // // 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
//遍歷全部seed節點 for (; i !== len && (elem = elems[i]) != null; i++) { if (byElement && elem) { j = 0; //遍歷全部過濾函數 while ((matcher = elementMatchers[j++])) { // if (matcher(elem, context, xml)) { results.push(elem); break; } } if (outermost) { dirruns = dirrunsUnique; cachedruns = ++matcherCachedRuns; } } } // Override manipulation of globals by nested matchers if (outermost) { dirruns = dirrunsUnique; outermostContext = contextBackup; } return unmatched; }; }
至此,$("#div_test > span input[checked=true]") 從頭至尾的流程就基本走通了。爲此咱們能夠得出幾個優化選擇器的結論:
1. 儘可能在選擇器以ID來查詢,或者至少開頭是以ID來查詢:這樣能夠快速縮小查詢的根節點。
2. 在Classe前面使用Tags:由於getElementsByTagName方法是第二快的查詢方法3. 在選擇器最後儘可能指定seed元素(千萬不能用*):由於Sizzle會從最後的選擇符開始尋找符合條件的seed集合4. 儘可能使用父子查詢來代替後代查詢:後代查詢須要循環查找,父子查詢範圍小不少。5. 緩存已查詢的jQuery對象:經過空間換時間的方式,不要每次都要執行過濾函數。