jQuery選擇器引擎和Sizzle介紹

1、前言

Sizzle原來是jQuery裏面的選擇器引擎,後來逐漸獨立出來,成爲一個獨立的模塊,能夠自由地引入到其餘類庫中。我曾經將其做爲YUI3裏面的一個module,用起來暢通無阻,沒有任何障礙。Sizzle發展到如今,以jQuery1.8爲分水嶺,大致上能夠分爲兩個階段,後面的版本中引入了編譯函數的概念,Sizzle的源碼變得更加難讀、再也不兼容低版本瀏覽器,並且看起來更加零散。本次閱讀的是Sizzle第一個階段的最終版本jQuery1.7,從中收穫頗多,一方面是框架設計的思路,另一方面是編程的技巧。css

2、jQuery constructor

Sizzle來源於jQuery,而且jQuery是一個基於DOM操做的類庫,那麼在研究Sizzle以前頗有必要看看jQuery的總體結構:html

 1 function(window, undefined) {
 2     var jQuery = function (selector, context) {
 3         return new  jQuery.fn.init(selector, context, rootjQuery);
 4     }
 5     jQuery.fn = jQuery.prototype = {...}
 6     jQuery.fn.init.prototype = jQuery.fn;
 7     // utilities method
 8     // Deferred 
 9     // support 
10     // Data cache
11     // queue
12     // Attribute
13     // Event
14     // Sizzle about 2k lines 
15     // DOM 
16     // css operation
17     // Ajax
18     // animation
19     // position account
20     window.jQuery  = window.$ = jQuery;
21 }

 

jQuery具備很強的工程性,一個接口能夠處理多種輸入,是jQuery容易上手的主要緣由,相應的,這樣一個功能龐大的API內部實現也是至關複雜。要想弄清楚jQuery與Sizzle之間的關係,首先就必須從jQuery的構造函數入手。通過整理,理清楚了構造函數的處理邏輯,在下面的表中,jQuery的構造函數要處理6大類狀況,可是隻有在處理選擇器表達式(selector expression)時纔會調用Sizzle選擇器引擎。前端

jQuery裏面的選擇器引擎:Sizzle介紹

3、Sizzle的設計思路

對於一個複雜的選擇器表達式(下文的討論前提是瀏覽器不支持querySelectorAll) ,如何對其進行處理?node

3.1 分割解析

對於複雜的選擇器表達式,原生的API沒法直接對其進行解析,可是卻能夠對其中的某些單元進行操做,那麼很天然就能夠採起先局部後總體的策略:把複雜的選擇器表達式拆分紅一個個塊表達式和塊間關係。在下圖中能夠看到,一、選擇器表達式是依據塊間關係進行分割拆分的;二、塊表達式裏面有不少僞類表達式,這是Sizzle的一大亮點,並且還能夠對僞類進行自定義,表現出很強的工程性;三、拆分後的塊表達式有多是簡單選擇器、屬性選擇器、僞類表達式的組合,例如div.a、.a[name = "beijing"]。正則表達式

jQuery裏面的選擇器引擎:Sizzle介紹

3.2 塊表達式查找

表達式拆分紅一個個塊表達式後,接下來的工做就是求出結果集合了。在3.1中已經聲明過,此時的塊表達式也多是複雜的選擇器表達式,那該怎麼處理組合的塊表達式呢?express

a. 依據API的性能查找:對於程序開發人員而言,代碼的效率是一個永恆的主題,那此時查詢的依據天然要依賴於選擇的性能。在DOM的API中,ID > Class > Name> Tag。編程

b. 塊內過濾:上述步驟中只是依據塊表達式的一部分進行了查詢,顯然獲得的集合範圍過大,有些不符合條件,那麼接下來就須要對上述獲得的元素集合進行塊內過濾。數組

總結:此環節包括兩個環節,查找+[過濾]。對於簡單的塊表達式,顯然是不須要過濾的。瀏覽器

3.3 塊間關係處理

通過塊內查找, 獲得了一個基本的元素集合,那如何處理塊間關係呢?經過觀察能夠發現,對一個複雜的選擇器表達式存在兩種順序:微信

  • 從左到右:對獲得的集合,進行內部逐個遍歷,獲得新的元素集合,只要還有剩餘的代碼塊,就須要不斷地重複查找、過濾的操做。總結下就是:屢次查找、過濾。
  • 從右到左:對獲得的元素集合,確定包括了最終的元素,並且還有多餘的、不符合條件的元素,那麼接下來的工做就是不斷過濾,把不符合條件的元素剔除掉。

jQuery裏面的選擇器引擎:Sizzle介紹

對於「相鄰的兄弟關係(+)」、「以後的兄弟關係(~)」,哪一種方式都無所謂了,效率沒什麼區別。可是對於「父子關係」、「祖前後代關係」就不同了,此時Sizzle選擇的是以從右到左爲主,下面從兩個維度進行解釋:

a、設計思路

  • 左到右:不斷查詢,不斷縮小上下文,不斷地獲得新的元素集合
  • 右到左:一次查詢,屢次過濾,第一查找獲得的元素集合不斷縮小,知道獲得最終的集合

b、DOM樹

  • 左到右:從DOM的上層往底層進行的,須要不斷遍歷子元素或後代元素,而一個元素節點的子元素或後代元素的個數是未知的或數量較多的
  • 右到左:從DOM的底層往上層進行的,須要不斷遍歷父元素或祖先元素,而一個元素的父元素或者祖先元素的數量是固定的或者有限的

可是從右到左是違背咱們的習慣的,這樣作到底會不會出現問題呢?答案是會出現錯誤,請看下面的一個簡單DOM樹:

1 <div>
2     <p>aa</p>
3 </div>
4 <div class=「content」>
5     <p>bb</p>
6     <p>cc</p>
7 </div>

 

求$(‘.content > p:first’)的元素集合?

首先進行分割: ‘.content > p:first’---> ['.content', '>', 'p:first']

右-->左
查找: A = $('p:first') = ['<p>aa</p>']
過濾: A.parent().isContainClass('content')---> null

在上面的例子中,咱們看到當選擇器表達式中存在位置僞類的時候,就會出現錯誤,這種狀況下沒有辦法,準確是第一位,只能選擇從左到右。

結論: 從性能觸發,採起從右到左; 爲了準確性,對位置僞類,只能採起從左到右。

4、Sizzle具體實現

4.1 Sizzle總體結構

1 if(document.querySelectorAll) {
2     sizzle = function(query, context) {
3         return makeArray(context.querySelectorAll(query));
4      }
5 } else {
6     sizzle 引擎實現,主要模擬querySelectorAll
7 }

 

經過上述代碼能夠看到,Sizzle選擇器引擎的主要工做就是向上兼容querySelectorAll這個API,假如全部瀏覽器都支持該API,那Sizzle就沒有存在的必要性了。

關鍵函數介紹:

  • Sizzle = function(selector, context, result, seed) : Sizzle引擎的入口函數
  • Sizzle.find: 主查找函數
  • Sizzle.filter: 主過濾函數
  • Sizzle.selectors.relative: 塊間關係處理函數集 {「+」: function() {}, 「 」:function() {}, 「>」: function() {}, 「~」: function() {}}

4.2 分割解析

chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g

就是靠這樣一個正則表達式,就能夠把複雜多樣的選擇器表達式分割成若干個塊表達式和塊間關係,是否是以爲塊表達式是一項神奇的技術,能夠把複雜問題抽象化。正則的缺點是不利於閱讀和維護,下圖對其進行圖形分析:

jQuery裏面的選擇器引擎:Sizzle介紹

再來看看是如何具體實現的呢:

 1 do {
 2         chunker.exec( "" ); // chunker.lastIndex = 0
 3         m = chunker.exec( soFar );
 4         if ( m ) {
 5             soFar = m[3];
 6             parts.push( m[1] );
 7             if ( m[2] ) {
 8                 extra = m[3];
 9                 break;
10             }
11         }
12     } while ( m );
13  
14 for example:
15 $(‘#J-con  ul>li:gt(2)’) 解析後的結果爲:
16 parts = ["#J-con", "ul", ">", 「li:gt(2)"]
17 extra = undefined
18     
19 $(‘#J-con  ul>li:gt(2), div.menu’) 解析後的結果爲:
20 parts = ["#J-con", "ul", ">", 「li:gt(2)"]
21 extra = ‘div.menu’

 

4.3 塊表達式處理

4.3.1 塊內查找

在查找環節,經過Sizzle.find來實現,主要邏輯以下:

  • 依據DOM API性能決定查找依據: ID > Class> Name> Tag, 其中要考慮瀏覽器是否支持getElementsByClassName
  • Expr.leftMatch:肯定塊表達式類型
  • Expr.find:具體的查找實現
  • 結果: {set: 結果集合, expr: 塊表達式剩餘的部分,用於下一步的塊內過濾}
// Expr.order = [「ID」, [ 「CLASS」], 「NAME」, 「TAG ]
for ( i = 0, len = Expr.order.length; i < len; i++ ) {
    ……
    if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
        set = Expr.find[ type ]( match, context);
        expr = expr.replace( Expr.match[ type ], "" );
    }
}

 

4.3.2塊內過濾

該過程經過Sizzle.filter來進行,該API不只能夠進行塊內過濾,還能夠進行塊間過濾,經過inplace參數來肯定。主要邏輯以下:

  • Expr.filter: {PSEUDO, CHILD, ID, TAG, CLASS, ATTR, POS} , 選則器表達式的類型
  • Expr.preFilter: 過濾前預處理,保證格式的規範化
  • Expr.filter: 過濾的具體實現對象
  • 內過濾、塊間從左到後: inplace=false,返回新對象;塊間從右到左: inplace=true, 原來的元素集合上過濾
Sizzle.filter = function( expr, set, inplace, not ) {
    for ( type in Expr.filter ) { //filter: {PSEUDO, CHILD, ID, TAG, CLASS, ATTR, POS}
        //   Expr.leftMatch:肯定selector的類型
        if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
            // 過濾前預處理,保證格式的規範化
            match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
            // 進行過濾操做    
            found = Expr.filter[ type ]( item, match, i, curLoop );
            // if inplace== true,獲得新數組對象;
            if ( inplace && found != null ) {
                if ( pass ) { anyFound = true; } else { curLoop[i] = false; }
            } else if ( pass ) {
                result.push( item );
            }
        }
    }
}

 

4.4 塊間關係處理

4.4.1 判斷處理順序

知足下面的正則,說明存在位置僞類,爲了保證計算的準肯定,必須採起從左到後的處理順序,不然能夠爲了效率盡情使用從右到左。

origPOS = /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/

jQuery裏面的選擇器引擎:Sizzle介紹

4.4.2 左到右處理

首先依據parts的第一個元素進行查詢,而後對獲得的元素集合進行遍歷,利用位置僞類處理函數posProcess進行僞類處理,直到數組parts爲空。

// parts是selector expression分割後的數組
set = Expr.relative[ parts[0] ] ? [ context ] :   Sizzle( parts.shift(), context );
// 對元素集合屢次遍歷,不斷查找
while(parts.length) {
 
    selector = parts.shift();
    ……
    set = posProcess(selector, set, seed);

}

 

接下來在看下posProcess的內部邏輯:若是表達式內部存在位置僞類(例如p:first),在DOM的API中不存在能夠處理僞類(:first)的API,這種狀況下就先把僞類剔除掉,依照剩餘的部分進行查詢(p),這樣獲得一個沒有僞類的元素集合,最後在以上述中的僞類爲條件,對獲得的元素集合進行過濾。

// 從左到後時,位置僞類處理方法
    var posProcess = function( selector, context, seed ) {
        var match,
            tmpSet = [],
            later = "",
            root = context.nodeType ? [context] : context;
        // 先剔除位置僞類,保存在later裏面
        while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
            later += match[0];
            selector = selector.replace( Expr.match.PSEUDO, "" );
        }
         
        selector = Expr.relative[selector] ? selector + "*" : selector;
        // 在不存在位置僞類的狀況下,進行查找
        for ( var i = 0, l = root.length; i < l; i++ ) {
            Sizzle( selector, root[i], tmpSet, seed );
        }
        // 以位置僞類爲條件,對結果集合進行過濾
        return Sizzle.filter( later, tmpSet );
    };

 

4.4.3 右到左的處理順序

其實Sizzle不徹底是採用從右到左,若是選擇器表達式的最左邊存在#id選擇器,就會首先對最左邊進行查詢,並將其做爲下一步的執行上下文,最終達到縮小上下文的目的,考慮的至關全面。

 1 // 若是selector expression 最左邊是#ID,則計算出#ID選擇器,縮小執行上下文
 2 if(parts[0] is #id) {
 3     context = Sizzle.find(parts.shift(), context)[0];
 4 }
 5 if (context) {
 6     // 獲得最後邊塊表達式的元素集合
 7     ret = Sizzle.find(parts.pop(), context);
 8     // 對於剛剛獲得的元素集合,進行塊內元素過濾
 9     set = Sizzle.filter(ret.expr, ret.set) ;
10     // 不斷過濾
11     while(parts.length) {
12         pop = parts.pop();
13         ……
14         Expr.relative[ cur ]( checkSet, pop );
15     }
16 }

 

對於塊間關係的過濾,主要依據Expr.relative來完成的。其處理邏輯關係是:判斷此時的選擇器表達式是否爲tag,若是是則直接比較nodeName,效率大增,不然只能調用Sizzle.filter。下面以相鄰的兄弟關係爲例進行說明:

"+": function(checkSet, part){
    var isPartStr = typeof part === "string",
        isTag = isPartStr && !rNonWord.test( part ),   //判斷過濾selector是否爲標籤選擇器
        isPartStrNotTag = isPartStr && !isTag;
    if ( isTag ) {
        part = part.toLowerCase();
    }
    for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
        if ( (elem = checkSet[i]) ) {
            while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
            checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
                elem || false :
                elem === part;
        }
    }
    if ( isPartStrNotTag ) {
        Sizzle.filter( part, checkSet, true );
    }
}

 

4.5 拓展性

Sizzle的另一大特性就是能夠自定義選擇器,固然僅限於僞類,這是Sizzle工程型很強的另一種表現:

jQuery裏面的選擇器引擎:Sizzle介紹

$.extend($.selectors.filters, {
    hasLi: function( elem ) {
        return $(elem).find('li').size() > 0;
    }
});
var e = $('#J-con :hasLi');
console.log(e.size());    // 1

 

上述代碼中的$.extend至關遠YUI3中的augment、extend、mix的合體,功能至關強大,只須要對$.selectors.filters(即Sizzle.selectors.filters)對象
進行拓展就能夠,裏面每一個屬性的返回值爲Boolean類型,用於判斷僞類的類型。

>>> Web前端頻道微信號:FrontDev, 掃描加關注,碎片時間提高前端開發技能! 

原文出處: bigbrother1984 

相關文章
相關標籤/搜索