jQuery中的Sizzle引擎分析

我分析的jQuery版本是1.8.3。Sizzle代碼從3669行開始到5358行,將近2000行的代碼,這個引擎的版本仍是比較舊,最新的版本已經到v2.2.2了,代碼已經超過2000行了。而且還有個專門的Sizzle主頁css

從一個demo開始,HTML代碼以下:node

複製代碼

<div id="grand_father">
    <div id="father">
        <div id="child1" class="child">子集1</div>
        <div id="child2" class="child">子集2</div>
        <div id="child3" class="child">子集3</div>
        <input type="radio" id="radio1"/>
    </div></div>

複製代碼

而後JavaScript代碼以下:數組

var $nodes = $('div + input[type="radio"],div.child:first-child');
console.log($nodes);

1)返回的是一個jQuery對象,以下圖所示,而且匹配到了兩個標籤,一個div和radio,瀏覽器

2)右邊的div在0的位置,radio在1的位置,這說明jQuery的選擇器匹配是從右往左的!閉包


 

下面看一個流程圖,當我編寫了$('div + input[type="radio"],div.child:first-child')後發生的過程:ide

 

1、jQuery對象

對象是須要new一下才行的,可是jQuery只要$("xxx")後,就生成了一個對象。函數

1)jQuery構造函數this

在第42行,將會返回一個new對象:spa

jQuery = function( selector, context ) {    // The jQuery object is actually just the init constructor 'enhanced'
    return new jQuery.fn.init( selector, context, rootjQuery );
}

 

2)jQuery對象結構prototype

根據上面的返回對象的圖中能夠看到:

a. 對象的原型屬性__proto__指向的是函數jQuery的原型屬性prototype。__proto__ 是內部 [ [Prototype ]] ,原型鏈就是經過這個屬性來實現的。

b. 索引是0和1的,實際上是瀏覽器中的原生對象,咱們能夠搞個簡單的選擇器來驗證,例如$("#radio1"),代碼將會執行到140行

複製代碼

elem = document.getElementById(match[2]);// Check parentNode to catch when Blackberry 4.6 returns// nodes that are no longer in the document #6963if (elem && elem.parentNode) {  // Handle the case where IE and Opera return items
  // by name instead of ID
  if (elem.id !== match[2]) {    return rootjQuery.find(selector);
  }  // Otherwise, we inject the element directly into the jQuery object
  this.length = 1;  this[0] = elem;}this.context = document;this.selector = selector;return this;

複製代碼

 

2、select函數

5116行的select函數是引擎的入口:

1)在這裏引用了詞法分析函數tokenize。

2)當tokenize返回的Token集合數組只有一個的時候,將會尋找種子合集【經過一些原生DOM接口可獲取到】,在5147行中能夠看到:

複製代碼

/*完整的find在4089行,簡易的find以下:
Expr.find = {
  'ID': context.getElementById,
  'CLASS': context.getElementsByClassName,
  'NAME': context.getElementsByName,
  'TAG': context.getElementsByTagName
}             
*/if ((find = Expr.find[type])) {  // Search, expanding context for leading sibling combinators
  if ((seed = find(
      token.matches[0].replace(rbackslash, ""),
      rsibling.test(tokens[0].type) && context.parentNode || context,
      xml
    ))) {    //省略邏輯....  }
}

複製代碼

3)經過compile編譯函數,生成Token集合數組對應的匹配器,匹配後返回結果。

 

3、詞法分析

高級的瀏覽器會直接使用querySelectorAll方法選擇匹配。而低級的瀏覽器IE6或IE7等,就只能進入到jQuery的Sizzle引擎進行匹配。

爲了調試方便,我將5182行的代碼修改爲「!document.querySelectorAll」,讓高級瀏覽器也進入Sizzle引擎中匹配。

 

1)Token格式

4684行的tokenize函數最終返回的是Token集合數組,Token是一個String對象,格式以下:

String{0:'字符1',1:'字符2',....., type:'對應的Token類型【TAG,ID,CLASS,ATTR,CHILD,PSEUDO,NAME,>,+,空格,~】', matches:'正則匹配到的一個結構'}

type類型根據4150行的relative對象和4230行的新航道託福filter對象中的key值獲取。

 

2)返回的結果

'div + input[type="radio"],div.child:first-child'返回的數組以下:


上面返回的順序是從左往右,先input,而後是div。

 

3)tokenize函數的流程

上圖中有4個關係符號:

Expr.relative = {  ">": { dir: "parentNode", first: true },  " ": { dir: "parentNode" },  "+": { dir: "previousSibling", first: true },  "~": { dir: "previousSibling" }
}

結合上面的HTML結構:

1)grand_father與child1屬於祖宗與後代關係(空格表達)

2)father與child1屬於父子關係,也算是祖先與後代關係(>表達)

3)child1與child2屬於臨近兄弟關係(+表達)

4)child1與child2,child3都屬於普通兄弟關係(~表達)

 

4、編譯函數

把高級規則轉換成底層實現就叫編譯,好比高級語言到機器語言的過程就是編譯。一樣把抽象的css選擇語法轉變成具體的匹配函數的過程也是編譯。

 

1)matcherFromTokens

5080行的compile函數經過引用4931行的matcherFromTokens函數獲取Token集合對應的匹配器,引用代碼以下:

複製代碼

1 i = group.length;//從右往左2 while (i--) {3   cached = matcherFromTokens(group[i]);4   if (cached[expando]) {5     setMatchers.push(cached);6   } else {7     elementMatchers.push(cached);8   }9 }

複製代碼

返回了兩個函數數組,對應上面的Token集合數組,因爲是從右往左,因此與上面的Token集合數組反過來。【在4979行console.log(matchers)】


打開第一個值,會發現裏面還嵌套着不少閉包,閉包裏面又有閉包,這就是前面所說的大的匹配函數:


matcherFromTokens最後會引用4803行的elementMatcher,將上面的數組做爲參數傳遞過去。

上面示例代碼的第7行就在將函數插入到elementMatchers數組中。再傳遞給下面的matcherFromGroupMatchers函數。

 

2)matcherFromGroupMatchers

再引用4983行的matcherFromGroupMatchers函數生成終極匹配器,返回匹配結果。

這個函數將會return出來的一個curry化的函數,也就是4986行的superMatcher函數。

在4995行,superMatcher函數會根據參數seed 、expandContext和context肯定一個起始的查詢範圍:

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

有多是直接從seed種子集合中獲取,也有可能在context或者context的父節點範圍內。

這裏的context是「document」,也就是整個DOM樹【在5003行console.log(elems)】,elems結構以下:


能夠看出若是事先定義了content,就會把範圍縮小不少,利於匹配,例如jQuery能夠這樣寫:

$('div + input[type="radio"],div.child:first-child', $('#grand_father'))

 

在5007行開始過濾,elementMatchers參數就是上面返回的大匹配器。

之因此用for是由於選擇器(div + input[type="radio"],div.child:first-child)中有「,」號,因此是兩組大匹配器。

複製代碼

for (;(elem = elems[i]) != null; i++) {  //省略邏輯...
  for (j = 0; (matcher = elementMatchers[j]); j++) {    if (matcher(elem, context, xml)) {
      results.push(elem);      break;
    }
  }  //省略邏輯...}

複製代碼

 

大體過程就是這樣,裏面還有不少細節地方,這裏就不討論了,有興趣的能夠本身去打打斷點玩玩。

相關文章
相關標籤/搜索