標籤:elementmatchers
我分析的jQuery版本是1.8.3。Sizzle代碼從3669行開始到5358行,將近2000行的代碼,這個引擎的版本還是比較舊,最新的版本已經到v2.2.2了,代碼已經超過2000行了。並且還有個專門的Sizzle首頁。
從一個demo開始,HTML代碼如下:
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
<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>
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
然後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‘)後發生的過程:
650) this.width=650;" src="http://images2015.cnblogs.com/blog/211606/201512/211606-20151227135830046-4612755.png" style="border:0px;margin-top:10px;" />
一、jQuery對象
對象是需要new一下才行的,但是jQuery只要$("xxx")後,就產生了一個對象。
1)jQuery建構函式
在第42行,將會返回一個new對象:
jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor ‘enhanced‘ return new jQuery.fn.init( selector, context, rootjQuery );}
2)jQuery對象結構
根據上面的返回對象的圖中可以看到:
a. 對象的原型屬性__proto__指向的是函數jQuery的原型屬性prototype。__proto__ 是內部 [ [Prototype ]] ,原型鏈就是通過這個屬性來實現的。
b. 索引是0和1的,其實是瀏覽器中的原生對象,我們可以搞個簡單的選取器來驗證,例如$("#radio1"),代碼將會執行到140行
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
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;
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
二、select函數
5116行的select函數是引擎的入口:
1)在這裡引用了詞法分析函數tokenize。
2)當tokenize返回的Token集合數組只有一個的時候,將會尋找種子合集【通過一些原生DOM介面可擷取到】,在5147行中可以看到:
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
/*完整的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 ))) { //省略邏輯.... }}
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
3)通過compile編譯函數,產生Token集合數組對應的匹配器,匹配後返回結果。
三、詞法分析
進階的瀏覽器會直接使用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函數的流程
650) this.width=650;" src="http://images2015.cnblogs.com/blog/211606/201512/211606-20151227141508999-1569539853.png" style="border:0px;margin-top:10px;" />
中有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都屬於普通兄弟關係(~表達)
四、編譯函數
把進階規則轉換成底層實現就叫編譯,比如進階語言到機器語言的過程就是編譯。同樣把抽象的css選擇文法轉變成具體的匹配函數的過程也是編譯。
650) this.width=650;" src="http://images2015.cnblogs.com/blog/211606/201512/211606-20151227175715812-1682379792.png" style="border:0px;margin-top:10px;" />
1)matcherFromTokens
5080行的compile函數通過引用4931行的matcherFromTokens函數擷取Token集合對應的匹配器,引用代碼如下:
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
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 }
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
返回了兩個函數數組,對應上面的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)中有“,”號,所以是兩組大匹配器。
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
for (;(elem = elems[i]) != null; i++) { //省略邏輯... for (j = 0; (matcher = elementMatchers[j]); j++) { if (matcher(elem, context, xml)) { results.push(elem); break; } } //省略邏輯...}
650) this.width=650;" src="/img/fz.gif" alt="複製代碼" style="margin-top:10px;border:none;" />
大致過程就是這樣,裡面還有很多細節地方,這裡就不討論了,有興趣的可以自己去打打斷點玩玩。
jQuery中的Sizzle引擎分析