在用前兩篇講述完正規表達式、初始化、特性檢測之後,終于到了我們的正餐——Sizzle工廠函數!
Sizzle工廠函數有四個參數,
selector:選擇符
context:查找上下文
results:傳回的結果數組
seed:待選元素,剛開始是undefined,但有的情況下Sizzle會遞歸調用,故那個時候會将待選元素層層傳遞
當我們要使用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的具體内容我們明天再見~