這篇文章我會將
Sizzle
整個篩選元素的流程所有講解一遍。從它是如何找出種子集seed
,又是如何將token
轉換爲篩選規則,再到是如何經過規則進行篩選的全部流程。這裏我會經過一個例子來進行說明,由token
轉換爲篩選規則那裏很是的繞,尤爲是Sizzle
還有緩存的邏輯夾雜在其中,並且最複雜的實際上是緩存。我我的的描述可能並不能讓人聽得很明白,因此有興趣的人,能夠結合個人說明去看一下源碼,我到如今也是隻看懂了百分之八十多,緩存的相關代碼我並無理解的特別透徹,因此這裏我只給你們分析一下我本身所理解到的,整個選擇元素的主流程。javascript
Sizzle
並非從左向右依次進行選擇的,並非先選擇出'.container'
而後再去找其下的input
。這樣雖然看似合理,但實際上是很消耗時間的,由於根據DOM
樹的結構越往下分支越多,因此Sizzle
會先在選擇器的末尾找到一個種子集(也就是seed
),而後經過種子集一層一層往上判斷,是否符合條件。vue
那麼如何選擇seed
呢?這就是select
函數乾的事情了。java
這個函數,主要就作了兩件事。node
tokenize
seed
一個選擇字符串可能會存在多個關係選擇器,好比body p>input:disabled
。若是使用這些關係選擇器來做爲分割,咱們能夠獲得幾組選擇器,seed就是在最後一組選擇器中的元素選擇器, ID選擇器, 或者class選擇器
,若是當最後一組選擇器沒有這三個選擇器的話,那麼就沒有seed
。jquery
以上述例子爲例,seed
就是整個document
中的全部input
。數組
若是在setDocument的時候, support.getElementsByClass = false得話,那麼`seed`不包括class選擇器 緩存
select = Sizzle.select = function(selector, context, results, seed) { var i, tokens, token, type, find, compiled = type selector === 'function' && selector, match = !seed && tokenize( (selector = compiled.selector || selector) ); results = results || []; // 這裏指選擇字符串沒有逗號的狀況, if (match.length === 1) { tokens = match[0] = match[0].slice(0); if (tokens.length > 2 && documentIsHTML && Expr.relative[tokens[1].type] ) { context = (Expr.find["ID"](token.matches[0] .replace(runescape, funescape), context) || [])[0]; if (!context) { return results; } else if (compiled) { context = context.parentNode } selector = selector.silce(tokens.shift().value.length); } i = matchExpr['needsContxt'].test(selector) ? 0 : tokens.length; // 這裏開始找seed while (i--) { token = tokens[i]; // 從後向前, 若是碰到關係選擇器了,那就不找了 if (Expr.relative[(type = token.type)]) { break; } // Expr.find 最多隻有三個屬性,這個是在setDocument的時候設置的 // TAG CLASS ID if ((find = Expr.find[type])) { if ( (seed = find( token.matches[0].replace(runescape, funescape), rsibling.test(tokens[0].type) && testContext(context.parentNode) || context ) ) ) { tokens.splice(i, 1); // 因爲已經抽出了seed 因此要重組selector // 上面的例子跑到這裏 selector就會變成 '.container [type=text]' selector = seed.length && toSelector(tokens); if (!selector) { push.apply(results, seed); return results } break; } } } } (compiled || compile(selecotr, match)) ( seed, context, !documentIsHTML, results, !context || rsibling.test(selector) && testContext(context.parentNode) || context ); // 注意: 這裏並無return compile返回出來的閉包執行後的結果, 而是return 做爲參數穿進去的results return results; } 複製代碼
compile
其實並非生成規則的函數,它算是一個總入口,主要的功能是將生成的規則緩存,從緩存中查找是否已經有對應的規則,返回一個superMatch函數, superMatch函數是作篩選的函數。markdown
compile = Sizzle.compile = function(selector, match) { var i, setMatcher = [], elementMatchers = [], cached = compilerCache[selector + ' ']; if (!cache) { if (!match) { match = tokenize(selector); } i = match.length; // 注意: 這裏的match是整個二維數組, 是整個一個選擇組, 因此這裏只循環一次 while(i--) { cached = matcherFromTokens(match[i]); // 在複雜的選擇器的時候, 僞類函數會被標記, 這裏就是判斷是不是僞類 if (cached[expando]) { setMatchers.push(cached); } else { elementMatchers.push(cached); } } } // 緩存 cache = compilerCache( selector, // 這個函數返回superMatch函數 matcherFromGroupMatchers(elementMatchers, setMatchers) ) } 複製代碼
matcherFromTokens
會經過token
生成規則,流程是這樣的。它會先建立一個matchers
數組,並建立一個baseMathcer
函數,這個baseMatcher
通常狀況都爲true
。以後遍歷整個token
,只要沒有遇到關係操做符,就將對應的filter
函數推入matchers
中;當遇到了關係操做符,會先將已經在matchers
中的所有篩選函數,用elementMatcher
函數包裹在一塊兒,再使用addCombinator
做爲紐帶返回一個函數,取代以前的matchers
。如此循環,直到將整個token所有遍歷結束。addCombinator
主要的功能就是根據關係操做符來查找兄弟元素和父級元素。閉包
我會把matcherFromTokens
,elementMatcher
,addCombinator
這三個函數都放在下面。app
function matcherFromTokens(tokens) { var checkContext, matcher, j, len = tokens.length, // 判斷是不是關係操做符開頭 leadingRelative = Expr.relative[ tokens[0].type ], // 若是不是關係符開頭, 默認就是父祖集關係 implicitRelative = leadingRelative || Expr.relative[' '], i = leadingRelative ? 1 : 0, // 這裏就是baseMatcher // addCombinator中做爲參數的fn 就是 filter matchContext = addCombinator(function(elem) { return elem === checkContext; }, implicitRelative, true), matchAnyContext = addCombinator(function(elem) { return indexOf(checkContext, elem) > -1; }, implicitRelative, true), // 這個就是最後規則的合集, 它先把baseMatcher放到了合集裏面 // 通常狀況 (!leadingRelative && (xml || context !== outermostContext))會返回true 從而不去執行下面的函數 matchers = [ function(elem, context, xml) { var ret = (!leadingRelative && (xml || context !== outermostContext)) || ( ( checkContext = context ).nodeType ? matchContext(elem, context, xml) : matchAnyContext(elem, context, xml) ); checkContext = null; return ret; } ]; // 正向遍歷tokens for (; i < len; i++) { // 若是是關係符的話 if ((matcher = Expr.relative[tokens[i].type])) { // 先將以有的規則用elementMatcher包裹在一塊兒, 再用addCombinator建立關聯; // 生成的新的matcher代替原來所有的matcher matchers = [addCombinator(elementMatcher(matchers), matcher)]; // 若是是 TAG ATTR PESUDO ID CLASS CHILD } else { matcher = Expr.filter[tokens[i].type].apply(null, toekns[i].matches); // 若是是僞類, 這裏我嘗試了不少選擇器可是都沒有進入到這個if裏面 // 感受得是特別複雜的選擇器了 // 由於一直沒試出來, 因此就沒搞懂這裏究竟是幹啥的 if (matcher[expando]) { j = ++i; for (; j < len; j++) { if (Expr.relative[tokens[j].type]) { break; } } return setMatcher( i > 1 && elementMatcher(matchers), i > 1 && toSelector( 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); } } // 最後再用elementMatcher裹一層, 返回一個函數 return elementMatcher(matchers); } function addCombinator(matcher, combinator, base) { var dir = combinator.dir, skip = combinator.next, key = skip || dir, checkNonElements = base && key === 'parentNode', doneName = done++; // 若是是 > + 這兩個關係符 return combinator.first ? // 檢查最近的父級或者兄弟元素 function(elem, context, xml) { // 這個while循環elem是持續賦值的 // 這裏就是爲何說是紐帶的緣由了 // 在這裏循環以後, 找到的新元素放到以後的matcher裏面, 構成了經過seed一級一級向上查找的邏輯 while(elem = elem[dir]) { // 當遇到元素節點的時候 if (elem.nodeType === 1 || checkNonElements) { return matcher(elem, context, xml); } } return false; } : // 檢查所有父級或者兄弟元素 function(elem, context, xml) { var oldCahce, uniqueCache, outerCache, newCache = [dirruns, doneName]; if (xml) { while ((elem = elem[dir])) { if (elem.nodeType === 1 || checkNonElments) { if (matcher(elem, context, xml)) { return true; } } } } else { while ((elem = elem[dir])) { // 這一塊都是緩存 // 緩存纔是最讓人看不懂的 // 這一塊,我也是沒看的特別懂, 若是有人理解這裏, 能夠告知一下 // 蟹蟹 if (elem.nodeType === 1 || checkNonElements) { outerCache = elem[expando] || (elem[expando] = {}); uniqueCache = outerCache[elem.uniqueID] || (outerCache[elem.uniqueID] = {} ); if (skip && skip === elem.nodeName.toLowerCase()) { elem = elem[dir] || elem; } else if ( (oldCache = uniqueCache[key]) && oldCache[0] === dirruns && oldCache[1] === doneName) { return (newCache[2] = oldCache[2]); } else { // 這裏是不走緩存的 上面兩個if 應該都是從緩存中拿值 uniqueCache[key] = newCache; if ( (newCache[2] = matcher(elem, context, xml)) ) { return true } } } } } return false; } } // 這個方法就是把一堆matacher 揉成一個 function elementMatcher(matchers) { return matchers.length > 1 ? function(elem, context, xml) { var i = matchers.length; // 注意這裏是i-- 說明這裏是倒敘的 // 這就是像剝洋蔥同樣, 一層一層判斷規則 while (i--) { //只要有一個不知足, 直接返回false if (!matchers[i](elem, context, xml)) { return false; } return true; } } : matchers[0]; } 複製代碼
在跑完了matcherFromTokens
,咱們再回過頭來繼續看compile
,當compile
的所有的matcherFromTokens
都跑完之後,就只剩返回作緩存和返回matcherFromGroupMatchers
了。matcherFromGroupMatchers
函數返回superMatcher
函數,superMatcher
函數使用來遍歷seed
,經過以前matcherFromTokes
運行得到的規則,對seed
進行篩選。
function matcherFromGroupsMatchers(elementMatchers, setMatchers) { var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, superMatcher = function(seed, context, xml, results, outermost) { var elem, j, matcher, matchedCount = 0, i = '0', unmatched = seed && [], setMatched = [], contextBackup = outermostContext, // 若是沒有seed 那麼就拿文檔所有的元素當作seed elems = seed || byElement && Expr.find["TAG"]('*', outermost), // 緩存用 dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), len = elems.length; if (outermost) { // 這個outermostContext會在baseMatcher的時候用做判斷 outermostContext = context == document || context || outermost; } for (; i !== len && (elem = elems[i] != null); i++) { if(byElement && elem) { j = 0; if (!context && elem.ownerDoucment != document) { setDocumet(elem); xml = !documentIsHtml; } // elementMatches會出現多個的狀況就是有逗號的狀況 // 這個時候只要知足一組規則就能夠把當前的元素推到結果集中 // 我們的例子只有一組規則 while ( (matcher = elementMatches[j++]) ) { if (matcher(elem, context || document, xml)) { results.push(elem); break; } } // 緩存 if (outermost) { dirruns = dirrunsUnique; } } // 沒有被匹配的那些元素 if (bySet) { if ((elem = !matcher && elem)) { matchedCount--; } if (seed) { unmatched.push(elem); } } } matchedCount += i; // 這裏的邏輯我並不太太懂, 由於我嘗試的例子中, 並無走到這裏的 // 這應該也是複雜選擇器纔會出現, 我試過:not(:not)的嵌套, 也沒走到這裏 // 但願有懂的人 能講解一下 // 這裏若是沒走for循環的話, 那麼i 是字符串'0' 而matchedCount是數字0 // 再包括matchedCount會-- 有可能即便走了for循環 也會致使會不相等 if (bySet && i !== matchedCount) { j = 0; while((matcher = setMatchers[j++])) { matcher(unmatched, setMatched, context, xml); } if (seed) { if (matchedCount > 0) { while (i--) { if ( !(unmatched[i] | setMatched[i]) ) { setMatched[i] = pop.call(results); } } } setMatched = condense(setMatched); } push.apply(results, setMatched); if (outermost && !seed && setMatched.length > 0 && (matchedCount + setMatchers.length) > -1) { //排序 Sizzle.uniqueSort(results); } } if (outermost) { dirruns = dirrunsUnique; outermoustContext = contextBackup; } // 這裏雖然return的是unmatched 可是results纔是最終的結果, 在select函數中最後return的是做爲參數的result return unmatched; } return bySet ? // marFunction 就是給參數的函數打expando標記的 markFunction(superMatcher) : superMatcher; } 複製代碼
看Sizzle
大概看了2個月, 在2020年以前把大概流程所有都看通了,算是過年了。在最後這段查找中,Sizzle用了大量的閉包,大量的柯里化函數,爲了就是保證所有的filter
函數入參,都爲elem, context, xml
。這是我看的第一個庫,看完了真的收穫不少,最開始由於看看司徒大大的書,一時興起想把Sizzle
看完,期間也以爲太難了想放棄,可是最後磕磕絆絆終因而看下來了。此次看完了等把JavaScript框架設計
都看完,再把jquery源碼擼了,再擼vue,而後再過年。哈哈哈哈哈。