Sizzle源碼分析:三 篩選和編譯

好了有了以前的詞法分析過程,如今咱們來到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對象:經過空間換時間的方式,不要每次都要執行過濾函數。
相關文章
相關標籤/搜索