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

前言

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

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

Sizzle的選擇原理

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

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

Sizzle.select

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

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

一個選擇字符串可能會存在多個關係選擇器,好比body p>input:disabled。若是使用這些關係選擇器來做爲分割,咱們能夠獲得幾組選擇器,seed就是在最後一組選擇器中的元素選擇器, ID選擇器, 或者class選擇器,若是當最後一組選擇器沒有這三個選擇器的話,那麼就沒有seedjquery

以上述例子爲例,seed就是整個document中的全部input數組

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

例子的tokenize

函數

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主要的功能就是根據關係操做符來查找兄弟元素和父級元素。app

我會把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];
}
複製代碼

流程圖

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,而後再過年。哈哈哈哈哈。

相關文章
相關標籤/搜索