jQuery 2.0.3 源碼分析Sizzle引擎 - 超級匹配

jQuery 2.0.3 源碼分析Sizzle引擎 - 超級匹配

聲明:本文爲原創文章,如需轉載,請註明來源並保留原文鏈接Aaron,謝謝!

通過Expr.find[ type ]我們找出選擇器最右邊的最終seed種子合集

通過Sizzle.compile函數編譯器,我們把tokenize詞法元素編譯成閉包函數

超級匹配superMatcher,用佳的方式從seed種子集合篩選出需要的數據

也就是通過seed與compile的匹配,得出最終的結果了

 


 superMatcher 函數

這個方法並不是一個直接定義的方法,通過matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出來的一個curry化的函數,但是最後執行起重要作用的是它。

注意是compile()().

複製代碼
    compile( selector, match )(
        seed,
        context,
        !documentIsHTML,
        results,
        rsibling.test( selector ) && testContext( context.parentNode ) || context
    );
複製代碼

superMatcher方法會根據參數seed 、expandContext和context確定一個起始的查詢範圍

elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),

有可能是直接從seed中查詢過濾,也有可能在context或者context的父節點範圍內。如果不是從seed開始,那隻能把整個DOM樹節點取出來過濾了,把整個DOM樹節點取出來過濾了,它會先執行Expr.find["TAG"]( "*", outermost )這句代碼等到一個elems集合(數組合集)

context.getElementsByTagName( tag );

可以看出對於優化選擇器,最右邊應該寫一個作用域的搜索範圍context比較好

 

開始遍歷這個seed種子合集了

while ( (matcher = elementMatchers[j++]) ) {
    if ( matcher( elem, context, xml ) ) {
        results.push( elem );
        break;
    }
}

elementMatchers:就是通過分解詞法器生成的閉包函數了,也就是“終極匹配器”

爲什麼是while?

前面就提到了,tokenize選擇器是可以用過 “,”逗號分組 group,所以就就會有個合集的概念了

matcher就得到了每一個終極匹配器

通過代碼很能看出來matcher方法運行的結果都是bool值

對裏面的元素逐個使用預先生成的matcher方法做匹配,如果結果爲true的則直接將元素堆入返回結果集裏面。

 


matcher

matcher 就是 elementMatcher函數的包裝

整個匹配的核心就在這個裏面了

複製代碼
        function( elem, context, xml ) {
            var i = matchers.length;
            while ( i-- ) {
                if ( !matchers[i]( elem, context, xml ) ) {
                    return false;
                }
            }
            return true;
        } :
複製代碼

我們先來回顧下這個matchers的組合原理

這個地方是最繞的,也是最暈的,所以還是要深入的理解纔行哦

先上個簡單的流程圖:

畫的不好 哈哈

 

執行分解:

第一步:

div > p + div.aaron input[type="checkbox"]

從右邊剝離出原生API能使用的接口屬性

context.getElementsByTagName( input )

所以找到了input ,因爲只可以用 tag是查詢,但是此時結果是個合集,引入seed的概念,稱之爲種子合集

 

第二步:

div > p + div.aaron [type="checkbox"]'

重組選擇器,踢掉input,得到新的tokens詞法元素哈希表

 

第三步:

通過matcherFromTokens函數,然後根據 關係選擇器 【'>',"空","~","+"】拆分分組,因爲DOM中的節點都是存在關係的,所以引入

Expr.relative -> first:true 兩個關係的“緊密”程度, 用於組合最佳的篩選

一次按照如下順序解析並且編譯閉包函數

編譯規則:div > p + div.aaron [type="checkbox"]'
編譯成4組閉包函數,然後在前後在合併組合成一組
複製代碼
div >

p +

div.aaron 

input[type="checkbox"]
複製代碼

 

先看構造一組編譯函數


A: 抽出div元素, 對應的是TAG類型
B: 通過Expr.filter找到對應匹配的處理器,返回一個閉包處理器
如:TAG方法

複製代碼
        "TAG": function( nodeNameSelector ) {
            var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
            return nodeNameSelector === "*" ?
                function() { return true; } :
                function( elem ) {
                    return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
                };
        },
複製代碼

C:將返回的curry方法放入到matchers匹配器組中,繼續分解

D:抽出子元素選擇器 '>' ,對應的類型 type">" 

E:通過Expr.relative找到elementMatcher方法分組合並多個詞素的的編譯函數

複製代碼
    function( elem, context, xml ) {
            var i = matchers.length;
            while ( i-- ) {
                if ( !matchers[i]( elem, context, xml ) ) {
                    return false;
                }
            }
複製代碼

所以這裏其實就是執行了各自Expr.filter匹配中的的判斷方法了,看到這裏matcher方法原來運行的結果都是bool值,

所以這裏只返回了一個組合閉包,通過這個篩選閉包,各自處理自己內部的元素

F:返回的這個匹配器還是不夠的,因爲沒有規範搜索範圍的優先級,所以這時候還要引入addCombinator方法

G:根據Expr.relative -> first:true 兩個關係的“緊密”程度
如果是是親密關係addCombinator返回

複製代碼
function( elem, context, xml ) {
    while ( (elem = elem[ dir ]) ) {
        if ( elem.nodeType === 1 || checkNonElements ) {
            return matcher( elem, context, xml );
        }
    }
}
複製代碼

所以可見如果是緊密關係的位置詞素,找到第一個親密的節點,立馬就用終極匹配器判斷這個節點是否符合前面的規則

 

這是第一組終極匹配器的生成流程了

可見過程極其複雜,被包裝了三層

依次

addCombinator
elementMatcher
Expr.relative

 

三個方法嵌套處理出來的結構

 

然後繼續分解下一組,遇到關係選擇器又繼續依照以上的過程分解

但是有一個不同的地方,下一個分組會把上一個分組給一併合併了

所以整個關係就是一個依賴嵌套很深的結構

最終暴露出來的終極匹配器其實只有一個閉包,但是有內嵌很深的分組閉包了

依照從左邊往右依次生成閉包,然後把上一組閉包又push到下一組閉包

就跟棧是一種後進先出的數據結構一樣處理了

所以在最外層也就是

type=["checkbox"]

 


我們回到superMatcher方法的處理了

在遍歷seed種子合集,依次匹配matchers閉包函數,傳入每一個seed的元素與之匹配(這裏就是input),在對應的編譯處理器中通過對input的處理,找到最優匹配結果

複製代碼
function( elem, context, xml ) {
    var i = matchers.length;
    while ( i-- ) {
        if ( !matchers[i]( elem, context, xml ) ) {
            return false;
        }
    }
    return true;
} :
複製代碼

這裏注意了,是i--,從後往前找
所以第一次開始匹配的就是

check: "checkbox"
name: "type"
operator: "="

那麼就找到對應的Attr處理方法

複製代碼
//屬性元匹配器工廠
//name :屬性名
//operator :操作符
//check : 要檢查的值
//例如選擇器 [type="checkbox"]中,name="type" operator="=" check="checkbox"
Expr.filter["ATTR"] = function( name, operator, check ) {
 
  //返回一個元匹配器
  return function( elem ) {
    //先取出節點對應的屬性值
    var result = Sizzle.attr( elem, name );
 
    //看看屬性值有木有!
    if ( result == null ) {
      //如果操作符是不等號,返回真,因爲當前屬性爲空 是不等於任何值的
      return operator === "!=";
    }
    //如果沒有操作符,那就直接通過規則了
    if ( !operator ) {
      return true;
    }
 
    //轉成字符串
    result += "";
 
 
    return 
      //如果是等號,判斷目標值跟當前屬性值相等是否爲真
      operator === "=" ? result === check :
 
      //如果是不等號,判斷目標值跟當前屬性值不相等是否爲真
      operator === "!=" ? result !== check :
 
      //如果是起始相等,判斷目標值是否在當前屬性值的頭部
      operator === "^=" ? check && result.indexOf( check ) === 0 :
 
      //這樣解釋: lang*=en 匹配這樣 <html lang="xxxxenxxx">的節點
      operator === "*=" ? check && result.indexOf( check ) > -1 :
 
      //如果是末尾相等,判斷目標值是否在當前屬性值的末尾
      operator === "$=" ? check && result.slice( -check.length ) === check :
 
      //這樣解釋: lang~=en 匹配這樣 <html lang="zh_CN en">的節點
      operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
 
      //這樣解釋: lang=|en 匹配這樣 <html lang="en-US">的節點
      operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
      //其他情況的操作符號表示不匹配
      false;
  };
},
複製代碼

 

Sizzle.attr( elem, name )

傳入elem元素就是seed中的input元素,找到是否有'type'類型的屬性,

比如

<input type="text">"

所以第一次匹配input就出錯了,返回的type是text,而不是我們需要的'checkbox'
這裏返回的結果就是false,所以整個之後的處理就直接return了

 


 

繼續拿出第二個input

繼續上一個流程,這時候發現檢測到的屬性

var result = Sizzle.attr( elem, name );
result: "checkbox"

此時滿足第一條匹配,然後繼續 i = 0

!matchers[i]( elem, context, xml )

找到第0個編譯函數

addCombinator
複製代碼
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;
            if ( cache[1] === true ) {
                return true;
            }
        }
    }
}
複製代碼
如果是不緊密的位置關係
那麼一直匹配到true爲止
如祖宗關係的話,就一直找父親節點直到有一個祖先節點符合規則爲止
直接遞歸調用
matcher( elem, context, xml )

其實就是下一組閉包隊列了,傳入的上下文是 div.aaron,也就是<input type="checkbox"的父節點

複製代碼
function (elem, context, xml) {
                var i = matchers.length;
                //從右到左開始匹配
                while (i--) {
                    //如果有一個沒匹配中,那就說明該節點elem不符合規則
                    if (!matchers[i](elem, context, xml)) {
                        return false;
                    }
                }
                return true;
        }
複製代碼

依照上面的規則,這樣遞歸下去了,一層一層的匹配

 


可見它原來不是一層一層往下查,卻有點倒回去向上做匹配、過濾的意思。Expr裏面只有find和preFilter返回的是集合。

儘管到這裏暫時還帶着一點疑問,就是最後它爲什麼用的是逐個匹配、過濾的方法來得到結果集,但是我想Sizzle最基本的“編譯原理”應該已經解釋清楚了。

哥們,別光看不頂啊!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章