在用前兩篇講述完正則表達式、初始化、特性檢測以後,終於到了咱們的正餐——Sizzle工廠函數!javascript
Sizzle工廠函數有四個參數,html
selector:選擇符java
context:查找上下文node
results:返回的結果數組正則表達式
seed:待選元素,剛開始是undefined,但有的狀況下Sizzle會遞歸調用,故那個時候會將待選元素層層傳遞chrome
當咱們要使用Sizzle時,使用頻率最高的一般是直接指定單個id、class、tag來獲取(一般還指定查找上下文來加速這一過程),而這種狀況下Sizzle作了優化,當判斷是這三種狀況時,直接調用原生API來獲取元素。其次,最快的方法莫過於使用querySelectorAll了,也省去了後續的過濾等各類耗性能的操做。若上述手段都不生效,再採用複雜的查找、過濾等流程。數組
接下來咱們看看源代碼。瀏覽器
//對工具函數處理完畢,開始正式的Sizzle工廠函數 //這個seed有什麼用? //如今能夠回答這個問題了,由於這個Sizzle會遞歸調用,這裏的seed保留的是已通過粗選的待選元素 function Sizzle( selector, context, results, seed ) { console.log('Sizzle begin'); console.log('arguments(selector, context, results, seed):'); console.log(arguments); var match, elem, m, nodeType, // QSA vars i, groups, old, nid, newContext, newSelector; //若是(查找範圍的所屬文檔節點或查找範圍)不是當前文檔節點,則設置一下文檔節點各方面的能力 if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { console.log('Sizzle setDocument'); setDocument( context ); } context = context || document; results = results || []; //若是沒有選擇符或選擇符不是字符串,則直接返回結果。 if ( !selector || typeof selector !== "string" ) { return results; } //這種寫法壓縮了一行代碼,在用的時候再首次初始化。 if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { return []; } //若是沒seed才進入,有seed的話說明確定不是簡單匹配了,最後在return的時候才處理 if ( documentIsHTML && !seed ) { // Shortcuts //jQuery用的思想是先用簡單的方式來執行簡單selector //這個時候其實應該考慮正則表達式效率。 if ( (match = rquickExpr.exec( selector )) ) { // Speed-up: Sizzle("#ID") //match[1]存的是ID //match[2]存的是TAG //match[3]存的是CLASS if ( (m = match[1]) ) { if ( nodeType === 9 ) { elem = context.getElementById( m ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document (jQuery #6963) //黑莓4.6會返回不在DOM樹裏的節點。 if ( elem && elem.parentNode ) { // Handle the case where IE, Opera, and Webkit return items // by name instead of ID //有的時候會根據name來返回而不是ID if ( elem.id === m ) { results.push( elem ); return results; } } else { return results; } } else { // Context is not a document //這一行執行了多種行爲,能夠學習一下。 //先判斷context是否有所屬文檔節點,有的話則先用文檔節點的方法得到指定節點(由於只有document纔有getElementById)。 //得到指定節點以後,再檢查context是否包含指定節點,最後檢查指定節點的id。 //只有文檔節點纔有getElementById if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && contains( context, elem ) && elem.id === m ) { results.push( elem ); return results; } } // Speed-up: Sizzle("TAG") } else if ( match[2] ) { push.apply( results, context.getElementsByTagName( selector ) ); return results; // Speed-up: Sizzle(".CLASS") //要是瀏覽器沒有getElementsByClassName,Sizzle不作任何處理,而不是模仿一個比較慢的API } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); return results; } } // QSA path //要是有QSA,且沒有帶bug的QSA或者選擇符不匹配該bugQSA if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { nid = old = expando; newContext = context; newSelector = nodeType === 9 && selector; // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements //??????爲何這樣寫? //爲何要給context設置一個id? //詳情參見http://www.cnblogs.com/snandy/archive/2011/03/30/1999388.html if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { //這裏是把選擇符用詞法分析器拆成一個個詞元 //其實就是一個數據結構{value:"div",type:"TAG",matches:"div"} //多個詞元組成一個詞序列tokens,也是一個group //以選擇器中的逗號爲分隔符,多個group組成一個groups數組 console.log('Sizzle tokenize'); groups = tokenize( selector ); console.log('Sizzle tokenize results'+groups); //若是查找範圍有屬性ID節點則取出來,沒有則將ID設爲expando if ( (old = context.getAttribute("id")) ) { nid = old.replace( rescape, "\\$&" ); } else { context.setAttribute( "id", nid ); } //nid變爲[id="expando"]屬性選擇符,做爲後面選擇的查找範圍標示。 //爲何不用#expando的形式? nid = "[id='" + nid + "'] "; i = groups.length; while ( i-- ) { groups[i] = nid + toSelector( groups[i] ); } //若是是要查找兄弟元素,則將查找範圍設爲原查找範圍的父元素 newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; newSelector = groups.join(","); } /*由於我是用的是chrome瀏覽器,測試的時候暫時註釋掉這個 if ( newSelector ) { try { push.apply( results, newContext.querySelectorAll( newSelector ) ); return results; } catch(qsaError) { } finally { if ( !old ) { context.removeAttribute("id"); } } }*/ } } // All others //若是上述省時省力的方法都不行的話,則使用真正複雜的方法進行查找 return select( selector.replace( rtrim, "$1" ), context, results, seed ); }
中間調用了tokenize這個工具函數,把selector轉換成groups數組,每個group又是一個tokens數組,tokens數組由以前說過的一個個token組成。咱們來看一下這個工具函數是怎麼切割selector的(其實簡單來講就是用正則表達式不斷地匹配、切割,用剩下的selector再匹配,再切割)緩存
//之後遇到這種工具函數,先拷到外面看輸入輸出 //當tokenize第二個參數爲true時,僅僅返回處理的結果長度 function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; if ( cached ) { //??????爲何這裏要調用一下slice?這裏是數組的slice //若是有緩存,parseOnly爲true,爲何不返回長度而是0 return parseOnly ? 0 : cached.slice( 0 ); } //soFar用來存切割剩下的selector soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run //本來這裏的寫法是!matched || (match = rcomma.exec( soFar ) //這裏的寫法應該換一下,換成(match = rcomma.exec( soFar ) || !matched //不然$(',body',document.documentElement)這樣的寫法會報錯 if ( (match = rcomma.exec( soFar )) || !matched ) { //第一次循環不進入 if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( (tokens = []) ); } matched = false; // Combinators //先執行看有沒有鏈接符[>+~] if ( (match = rcombinators.exec( soFar )) ) { matched = match.shift(); tokens.push({ value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) }); //我切我切 soFar = soFar.slice( matched.length ); } // Filters for ( type in Expr.filter ) { //每一次循環都要用過濾器濾一遍 //如有預處理過濾器,則執行預處理過濾器的寫法 !preFilters[ type ] || (match = preFilters[ type ]( match ))) if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); tokens.push({ value: matched, type: type, matches: match }); soFar = soFar.slice( matched.length ); } } //當matched再也沒有捕獲到的元素了,則能夠跳出 if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }
這裏還有一個工具函數,toSelector,和上面的tokenize的做用恰好相反,即將一個個token給再拼成selector,過程很簡單,不放代碼了,本文最後再看一個select函數,用於調用各個查找函數(find),來找到待選集seed。數據結構
function select( selector, context, results, seed ) { console.log('select begin'); console.log('arguments:selector, context, results, seed'); console.log(arguments); console.log('select tokenize'); var i, tokens, token, type, find, match = tokenize( selector ); console.log('select after tokenize'); console.log(match); if ( !seed ) { // Try to minimize operations if there is only one group //嘗試最小化操做? //若是隻有一個group if ( match.length === 1 ) { // Take a shortcut and set the context if the root selector is an ID tokens = match[0] = match[0].slice( 0 ); //若是token的數量大於2,且第一個token的類型是id if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { console.log('select find id'); context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; if ( !context ) { return results; } //減去已經用掉的長度 selector = selector.slice( tokens.shift().value.length ); } // Fetch a seed set for right-to-left matching //先檢查一下看selector是否必需要查找上下文,好比上來就使用 >之類的鏈接符或:nth(1)之類的僞方法 i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; //若是selector不須要查找上下文,則直接進入下面的循環進行查找seed,不然跳過,交給後面的compile去遞歸得到seed。 while ( i-- ) { token = tokens[i]; // Abort if we hit a combinator //?????鏈接符會怎樣? if ( Expr.relative[ (type = token.type) ] ) { break; } if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators console.log('select after find'); if ( (seed = find( token.matches[0].replace( runescape, funescape ), rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context )) ) { console.log('select after find:seed'); console.log(seed); // If seed is empty or no tokens remain, we can return early //由於是從右向左匹配的,範圍會越縮越小,若是這都得到不了seed,說明再縮小範圍也沒意義 //刪除掉已經使用過的token tokens.splice( i, 1 ); selector = seed.length && toSelector( tokens ); console.log(selector); //若是seed.length > 0 且全部token都用完了,則能夠直接返回了 if ( !selector ) { push.apply( results, seed ); return results; } //若是找到了seed,還有沒用完的token要過濾,則跳出循環,執行下面的compile break; } } } } } // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above //編譯好的matcher串,參數爲seed,context,xml,result,outermostContext console.log('select compile'); compile( selector, match )( seed, context, !documentIsHTML, results, rsibling.test( selector ) && testContext( context.parentNode ) || context ); console.log('select after compile'); console.log(results); return results; }
這裏最後調用的compile就是咱們用來編譯匹配函數matcher的編譯函數了,這裏的過程是:先進行編譯,得到一個matcher,再調用,將種種參數傳入
compile的具體內容咱們明天再見~