一、前言
Sizzle原來是jQuery里面的選擇器引擎,后來逐漸獨立出來,成為一個獨立的模塊,可以自由地引入到其他類庫中。我曾經將其作為YUI3里面的一個module,用起來暢通無阻,沒有任何障礙。Sizzle發展到現在,以jQuery1.8為分水嶺,大體上可以分為兩個階段,后面的版本中引入了編譯函數的概念,Sizzle的源碼變得更加難讀、不再兼容低版本瀏覽器,而且看起來更加零散。本次閱讀的是Sizzle第一個階段的最終版本jQuery1.7,從中收獲頗多,一方面是框架設計的思路,另外一方面是編程的技巧。
二、jQuery constructor
Sizzle來源於jQuery,並且jQuery是一個基於DOM操作的類庫,那么在研究Sizzle之前很有必要看看jQuery的整體結構:
(function(window, undefined) { var jQuery = function (selector, context) { return new jQuery.fn.init(selector, context, rootjQuery); } jQuery.fn = jQuery.prototype = {...} jQuery.fn.init.prototype = jQuery.fn; // utilities method // Deferred // support // Data cache // queue // Attribute // Event // Sizzle about 2k lines // DOM // css operation // Ajax // animation // position account window.jQuery = window.$ = jQuery; })
jQuery具有很強的工程性,一個接口可以處理多種輸入,是jQuery容易上手的主要原因,相應的,這樣一個功能龐大的API內部實現也是相當復雜。要想弄清楚jQuery與Sizzle之間的關系,首先就必須從jQuery的構造函數入手。經過整理,理清楚了構造函數的處理邏輯,在下面的表中,jQuery的構造函數要處理6大類情況,但是只有在處理選擇器表達式(selector expression)時才會調用Sizzle選擇器引擎。
三、Sizzle的設計思路
對於一個復雜的選擇器表達式(下文的討論前提是瀏覽器不支持querySelectorAll) ,如何對其進行處理?
3.1 分割解析
對於復雜的選擇器表達式,原生的API無法直接對其進行解析,但是卻可以對其中的某些單元進行操作,那么很自然就可以采取先局部后整體的策略:把復雜的選擇器表達式拆分成一個個塊表達式和塊間關系。在下圖中可以看到,1、選擇器表達式是依據塊間關系進行分割拆分的;2、塊表達式里面有很多偽類表達式,這是Sizzle的一大亮點,而且還可以對偽類進行自定義,表現出很強的工程性;3、拆分后的塊表達式有可能是簡單選擇器、屬性選擇器、偽類表達式的組合,例如div.a、.a[name = "beijing"]。
3.2 塊表達式查找
表達式拆分成一個個塊表達式后,接下來的工作就是求出結果集合了。在3.1中已經聲明過,此時的塊表達式也可能是復雜的選擇器表達式,那該怎么處理組合的塊表達式呢?
a. 依據API的性能查找:對於程序開發人員而言,代碼的效率是一個永恆的主題,那此時查詢的依據自然要依賴於選擇的性能。在DOM的API中,ID > Class > Name> Tag。
b. 塊內過濾:上述步驟中只是依據塊表達式的一部分進行了查詢,顯然得到的集合范圍過大,有些不符合條件,那么接下來就需要對上述得到的元素集合進行塊內過濾。
總結:此環節包括兩個環節,查找+[過濾]。對於簡單的塊表達式,顯然是不需要過濾的。
3.3 塊間關系處理
經過塊內查找, 得到了一個基本的元素集合,那如何處理塊間關系呢?通過觀察可以發現,對一個復雜的選擇器表達式存在兩種順序:
- 從左到右:對得到的集合,進行內部逐個遍歷,得到新的元素集合,只要還有剩余的代碼塊,就需要不斷地重復查找、過濾的操作。總結下就是:多次查找、過濾。
- 從右到左:對得到的元素集合,肯定包括了最終的元素,而且還有多余的、不符合條件的元素,那么接下來的工作就是不斷過濾,把不符合條件的元素剔除掉。
對於“相鄰的兄弟關系(+)”、“之后的兄弟關系(~)”,哪種方式都無所謂了,效率沒什么區別。但是對於“父子關系”、“祖先后代關系”就不一樣了,此時Sizzle選擇的是以從右到左為主,下面從兩個維度進行解釋:
a、設計思路
- 左到右:不斷查詢,不斷縮小上下文,不斷地得到新的元素集合
- 右到左:一次查詢,多次過濾,第一查找得到的元素集合不斷縮小,知道得到最終的集合
b、DOM樹
- 左到右:從DOM的上層往底層進行的,需要不斷遍歷子元素或后代元素,而一個元素節點的子元素或后代元素的個數是未知的或數量較多的
- 右到左:從DOM的底層往上層進行的,需要不斷遍歷父元素或祖先元素,而一個元素的父元素或者祖先元素的數量是固定的或者有限的
但是從右到左是違背我們的習慣的,這樣做到底會不會出現問題呢?答案是會出現錯誤,請看下面的一個簡單DOM樹:
<div> <p>aa</p> </div> <div class=“content”> <p>bb</p> <p>cc</p> </div> 求$(‘.content > p:first’)的元素集合? 首先進行分割: ‘.content > p:first’---> ['.content', '>', 'p:first'] 右-->左 查找: A = $('p:first') = ['<p>aa</p>'] 過濾: A.parent().isContainClass('content')---> null
在上面的例子中,我們看到當選擇器表達式中存在位置偽類的時候,就會出現錯誤,這種情況下沒有辦法,准確是第一位,只能選擇從左到右。
結論: 從性能觸發,采取從右到左; 為了准確性,對位置偽類,只能采取從左到右。
四、Sizzle具體實現
4.1 Sizzle整體結構
if(document.querySelectorAll) { sizzle = function(query, context) { return makeArray(context.querySelectorAll(query)); } } else { sizzle 引擎實現,主要模擬querySelectorAll }
通過上述代碼可以看到,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
就是靠這樣一個正則表達式,就可以把復雜多樣的選擇器表達式分割成若干個塊表達式和塊間關系,是不是覺得塊表達式是一項神奇的技術,可以把復雜問題抽象化。正則的缺點是不利於閱讀和維護,下圖對其進行圖形分析:
再來看看是如何具體實現的呢:
do { chunker.exec( "" ); // chunker.lastIndex = 0 m = chunker.exec( soFar ); if ( m ) { soFar = m[3]; parts.push( m[1] ); if ( m[2] ) { extra = m[3]; break; } } } while ( m ); for example: $(‘#J-con ul>li:gt(2)’) 解析后的結果為: parts = ["#J-con", "ul", ">", “li:gt(2)"] extra = undefined $(‘#J-con ul>li:gt(2), div.menu’) 解析后的結果為: parts = ["#J-con", "ul", ">", “li:gt(2)"] 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*)\))?(?=[^\-]|$)/
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選擇器,就會首先對最左邊進行查詢,並將其作為下一步的執行上下文,最終達到縮小上下文的目的,考慮的相當全面。
// 如果selector expression 最左邊是#ID,則計算出#ID選擇器,縮小執行上下文 if(parts[0] is #id) { context = Sizzle.find(parts.shift(), context)[0]; } if (context) { // 得到最后邊塊表達式的元素集合 ret = Sizzle.find(parts.pop(), context); // 對於剛剛得到的元素集合,進行塊內元素過濾 set = Sizzle.filter(ret.expr, ret.set) ; // 不斷過濾 while(parts.length) { pop = parts.pop(); …… Expr.relative[ cur ]( checkSet, pop ); } }
對於塊間關系的過濾,主要依據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工程型很強的另外一種表現:
$.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)對象