Sizzle源碼分析(四):Sizzle是如何選擇元素的

NO IMAGE

前言

這篇文章我會將Sizzle整個篩選元素的流程全部講解一遍。從它是如何找出種子集seed,又是如何將token轉換為篩選規則,再到是如何通過規則進行篩選的所有流程。這裡我會通過一個例子來進行說明,由token轉換為篩選規則那裡非常的繞,尤其是Sizzle還有緩存的邏輯夾雜在其中,而且最複雜的其實是緩存。我個人的描述可能並不能讓人聽得很明白,所以有興趣的人,可以結合我的說明去看一下源碼,我到現在也是隻看懂了百分之八十多,緩存的相關代碼我並沒有理解的特別透徹,所以這裡我只給大家分析一下我自己所理解到的,整個選擇元素的主流程。

例子: Sizzle(‘.container input[type=text]’)

Sizzle的選擇原理

Sizzle並不是從左向右依次進行選擇的,並不是先選擇出'.container'然後再去找其下的input。這樣雖然看似合理,但其實是很消耗時間的,因為根據DOM樹的結構越往下分支越多,所以Sizzle會先在選擇器的末尾找到一個種子集(也就是seed),然後通過種子集一層一層往上判斷,是否符合條件。

那麼如何選擇seed呢?這就是select函數乾的事情了。

Sizzle.select

這個函數,主要就做了兩件事。

  1. 將選擇器字符串 tokenize
  2. 找出seed

一個選擇字符串可能會存在多個關係選擇器,比如body p>input:disabled。如果使用這些關係選擇器來作為分割,我們可以得到幾組選擇器,seed就是在最後一組選擇器中的元素選擇器, ID選擇器, 或者class選擇器,如果當最後一組選擇器沒有這三個選擇器的話,那麼就沒有seed

以上述例子為例,seed就是整個document中的所有input

如果在setDocument的時候, support.getElementsByClass = false得話,那麼`seed`不包括class選擇器

例子的tokenize

Sizzle源碼分析(四):Sizzle是如何選擇元素的

函數

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

compile其實並不是生成規則的函數,它算是一個總入口,主要的功能是將生成的規則緩存,從緩存中查找是否已經有對應的規則,返回一個superMatch函數, superMatch函數是做篩選的函數。

函數

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

matcherFromTokens會通過token生成規則,流程是這樣的。它會先創建一個matchers數組,並創建一個baseMathcer函數,這個baseMatcher一般情況都為true。之後遍歷整個token,只要沒有遇到關係操作符,就將對應的filter函數推入matchers中;當遇到了關係操作符,會先將已經在matchers中的全部篩選函數,用elementMatcher函數包裹在一起,再使用addCombinator作為紐帶返回一個函數,取代之前的matchers。如此循環,直到將整個token全部遍歷結束。addCombinator主要的功能就是根據關係操作符來查找兄弟元素和父級元素。

我會把matcherFromTokenselementMatcheraddCombinator這三個函數都放在下面。

函數

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];
}

流程圖

Sizzle源碼分析(四):Sizzle是如何選擇元素的

matcherFromGroupMatchers

在跑完了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,然後再過年。哈哈哈哈哈。

相關文章

HashMap原理技術知識整理

SpringMVC加載流程

Spring中FactoryBean的作用和實現原理

基於Charles,如何利用代理技術進行接口mock測試