Sizzle原來是jQuery裏面的選擇器引擎,後來逐漸獨立出來,成爲一個獨立的模塊,能夠自由地引入到其餘類庫中。我曾經將其做爲YUI3裏面的一個module,用起來暢通無阻,沒有任何障礙。Sizzle發展到如今,以jQuery1.8爲分水嶺,大致上能夠分爲兩個階段,後面的版本中引入了編譯函數的概念,Sizzle的源碼變得更加難讀、再也不兼容低版本瀏覽器,並且看起來更加零散。本次閱讀的是Sizzle第一個階段的最終版本jQuery1.7,從中收穫頗多,一方面是框架設計的思路,另一方面是編程的技巧。css
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選擇器引擎。前端
對於一個複雜的選擇器表達式(下文的討論前提是瀏覽器不支持querySelectorAll) ,如何對其進行處理?node
對於複雜的選擇器表達式,原生的API沒法直接對其進行解析,可是卻能夠對其中的某些單元進行操做,那麼很天然就能夠採起先局部後總體的策略:把複雜的選擇器表達式拆分紅一個個塊表達式和塊間關係。在下圖中能夠看到,一、選擇器表達式是依據塊間關係進行分割拆分的;二、塊表達式裏面有不少僞類表達式,這是Sizzle的一大亮點,並且還能夠對僞類進行自定義,表現出很強的工程性;三、拆分後的塊表達式有多是簡單選擇器、屬性選擇器、僞類表達式的組合,例如div.a、.a[name = "beijing"]。正則表達式
表達式拆分紅一個個塊表達式後,接下來的工做就是求出結果集合了。在3.1中已經聲明過,此時的塊表達式也多是複雜的選擇器表達式,那該怎麼處理組合的塊表達式呢?express
a. 依據API的性能查找:對於程序開發人員而言,代碼的效率是一個永恆的主題,那此時查詢的依據天然要依賴於選擇的性能。在DOM的API中,ID > Class > Name> Tag。編程
b. 塊內過濾:上述步驟中只是依據塊表達式的一部分進行了查詢,顯然獲得的集合範圍過大,有些不符合條件,那麼接下來就須要對上述獲得的元素集合進行塊內過濾。數組
總結:此環節包括兩個環節,查找+[過濾]。對於簡單的塊表達式,顯然是不須要過濾的。瀏覽器
通過塊內查找, 獲得了一個基本的元素集合,那如何處理塊間關係呢?經過觀察能夠發現,對一個複雜的選擇器表達式存在兩種順序:微信
對於「相鄰的兄弟關係(+)」、「以後的兄弟關係(~)」,哪一種方式都無所謂了,效率沒什麼區別。可是對於「父子關係」、「祖前後代關係」就不同了,此時Sizzle選擇的是以從右到左爲主,下面從兩個維度進行解釋:
a、設計思路
b、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
在上面的例子中,咱們看到當選擇器表達式中存在位置僞類的時候,就會出現錯誤,這種狀況下沒有辦法,準確是第一位,只能選擇從左到右。
結論: 從性能觸發,採起從右到左; 爲了準確性,對位置僞類,只能採起從左到右。
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就沒有存在的必要性了。
關鍵函數介紹:
chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g
就是靠這樣一個正則表達式,就能夠把複雜多樣的選擇器表達式分割成若干個塊表達式和塊間關係,是否是以爲塊表達式是一項神奇的技術,能夠把複雜問題抽象化。正則的缺點是不利於閱讀和維護,下圖對其進行圖形分析:
再來看看是如何具體實現的呢:
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’
在查找環節,經過Sizzle.find來實現,主要邏輯以下:
// 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 ], "" ); } }
該過程經過Sizzle.filter來進行,該API不只能夠進行塊內過濾,還能夠進行塊間過濾,經過inplace參數來肯定。主要邏輯以下:
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 ); } } } }
知足下面的正則,說明存在位置僞類,爲了保證計算的準肯定,必須採起從左到後的處理順序,不然能夠爲了效率盡情使用從右到左。
origPOS = /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/
首先依據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 ); };
其實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 ); } }
Sizzle的另一大特性就是能夠自定義選擇器,固然僅限於僞類,這是Sizzle工程型很強的另一種表現:
$.extend($.selectors.filters, { hasLi: function( elem ) { return $(elem).find('li').size() > 0; } }); var e = $('#J-con :hasLi'); console.log(e.size()); // 1
>>> Web前端頻道微信號:FrontDev, 掃描加關注,碎片時間提高前端開發技能!