聲明:本文為原創文章,如需轉載,請注明來源並保留原文鏈接Aaron,謝謝!
先來回答博友的提問:
如何解析
div > p + div.aaron input[type="checkbox"]
順便在深入理解下解析的原理:
HTML結構
<div id="text"> <p> <input type="text" /> </p> <div class="aaron"> <input type="checkbox" name="readme" value="Submit" /> <p>Sizzle</p> </div> </div>
選擇器語句
div > p + div.aaron input[type="checkbox"]
組合后的意思大概就是:
1. 選擇父元素為 <div> 元素的所有子元素 <p> 元素
2. 選擇緊接在 <p> 元素之后的所有 <div> 並且class="aaron " 的所有元素
3. 之后選擇 div.aaron 元素內部的所有 input並且帶有 type="checkbox" 的元素
就針對這個簡單的結構,我們實際中是不可能這么寫的,但是這里我用簡單的結構,描述出復雜的處理
我們用組合語句,jquery中,在高級瀏覽器上都是用過querySelectorAll處理的,所以我們討論的都是在低版本上的實現,偽類選擇器,XML 要放到后最后,本文暫不涉及這方便的處理.
需要用到的幾個知識點:
1: CSS選擇器的位置關系
2: CSS的瀏覽器實現的基本接口
3: CSS選擇器從右到左掃描匹配
CSS選擇器的位置關系
文檔中的所有節點之間都存在這樣或者那樣的關系
其實不難發現,一個節點跟另一個節點有以下幾種關系:
祖宗和后代
父親和兒子
臨近兄弟
普通兄弟
在CSS選擇器里邊分別是用:空格;>;+;~
(其實還有一種關系:div.aaron,中間沒有空格表示了選取一個class為aaron的div節點)
<div id="grandfather"> <div id="father"> <div id="child1"></div> <div id="child2"></div> <div id="child3"></div> </div> </div>
- 爺爺grandfather與孫子child1屬於祖宗與后代關系(空格表達)
- 父親father與兒子child1屬於父子關系,也算是祖先與后代關系(>表達)
- 哥哥child1與弟弟child2屬於臨近兄弟關系(+表達)
- 哥哥child1與弟弟child2,弟弟child3都屬於普通兄弟關系(~表達)
在Sizzle里有一個對象是記錄跟選擇器相關的屬性以及操作:Expr。它有以下屬性:
relative = { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }
所以在Expr.relative里邊定義了一個first屬性,用來標識兩個節點的“緊密”程度,例如父子關系和臨近兄弟關系就是緊密的。在創建位置匹配器時,會根據first屬性來匹配合適的節點。
CSS的瀏覽器實現的基本接口
除去querySelector,querySelectorAll
HTML文檔一共有這么四個API:
- getElementById,上下文只能是HTML文檔。
- getElementsByName,上下文只能是HTML文檔。
- getElementsByTagName,上下文可以是HTML文檔,XML文檔及元素節點。
- getElementsByClassName,上下文可以是HTML文檔及元素節點。IE8還沒有支持。
所以要兼容的話sizzle最終只會有三種完全靠譜的可用
Expr.find = { 'ID' : context.getElementById, 'CLASS' : context.getElementsByClassName, 'TAG' : context.getElementsByTagName }
CSS選擇器從右到左掃描匹配
接下我們就開始分析解析規則了
1. 選擇器語句
div > p + div.aaron input[type="checkbox"]
2. 開始通過詞法分析器tokenize分解對應的規則(這個上一章具體分析過了)
分解每一個小塊 type: "TAG" value: "div" matches .... type: ">" value: " > " type: "TAG" value: "p" matches .... type: "+" value: " + " type: "TAG" value: "div" matches .... type: "CLASS" value: ".aaron" matches .... type: " " value: " " type: "TAG" value: "input" matches .... type: "ATTR" value: "[type="checkbox"]" matches .... 除去關系選擇器,其余的有語意的標簽都都對應這分析出matches 比如 最后一個屬性選擇器分支 "[type="checkbox"]" matches = [ 0: "type" 1: "=" 2: "checkbox" ] type: "ATTR" value: "[type="checkbox"]"
所以就分解出了9個部分了
那么如何匹配才是最有效的方式?
3. 從右往左匹配
最終還是通過瀏覽器提供的API實現的, 所以Expr.find就是最終的實現接口了
首先確定的肯定是從右邊往左邊匹配,但是右邊第一個是
"[type="checkbox"]"
很明顯Expr.find 中不認識這種選擇器,所以只能在往前扒一個
趴到了
type: "TAG"
value: "input"
這種標簽Expr.find能匹配到了,所以直接調用
Expr.find["TAG"] = support.getElementsByTagName ? function(tag, context) { if (typeof context.getElementsByTagName !== strundefined) { return context.getElementsByTagName(tag); } } :
但是getElementsByTagName方法返回的是一個合集
所以
這里引入了seed - 種子合集(搜索器搜到符合條件的標簽),放入到這個初始集合seed中
OK了 這里暫停了,不在往下匹配了,在用這樣的方式往下匹配效率就慢了
開始整理:
重組一下選擇器,剔掉已經在用於處理的tag標簽,input
所以選擇器變成了:
selector: "div > p + div.aaron [type="checkbox"]"
這里可以優化下,如果直接剔除后,為空了,就證明滿足了匹配要求,直接返回結果了
到這一步為止
我們能夠使用的東東:
1 seed合集
2 通過tokenize分析解析規則組成match合集
本來是9個規則快,因為匹配input,所以要對應的也要踢掉一個所以就是8個了
3 選擇器語句,對應的踢掉了input
"div > p + div.aaron [type="checkbox"]"
此時send目標合集有2個最終元素了
那么如何用最簡單,最有效率的方式從2個條件中找到目標呢?
涉及的源碼:
//引擎的主要入口函數 function select(selector, context, results, seed) { var i, tokens, token, type, find, //解析出詞法格式 match = tokenize(selector); if (!seed) { //如果外界沒有指定初始集合seed了。 // Try to minimize operations if there is only one group // 沒有多組的情況下 // 如果只是單個選擇器的情況,也即是沒有逗號的情況:div, p,可以特殊優化一下 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序列 //如果第一個是selector是id我們可以設置context快速查找 if (tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && Expr.relative[tokens[1].type]) { context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0]; if (!context) { //如果context這個元素(selector第一個id選擇器)都不存在就不用查找了 return results; } //去掉第一個id選擇器 selector = selector.slice(tokens.shift().value.length); } // Fetch a seed set for right-to-left matching //其中: "needsContext"= new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) //即是表示如果沒有一些結構偽類,這些是需要用另一種方式過濾,在之后文章再詳細剖析。 //那么就從最后一條規則開始,先找出seed集合 i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length; //從右向左邊查詢 while (i--) { //從后開始向前找! token = tokens[i]; //找到后邊的規則 // Abort if we hit a combinator // 如果遇到了關系選擇器中止 // // > + ~ 空 // if (Expr.relative[(type = token.type)]) { break; } /* 先看看有沒有搜索器find,搜索器就是瀏覽器一些原生的取DOM接口,簡單的表述就是以下對象了 Expr.find = { 'ID' : context.getElementById, 'CLASS' : context.getElementsByClassName, 'NAME' : context.getElementsByName, 'TAG' : context.getElementsByTagName } */ //如果是:first-child這類偽類就沒有對應的搜索器了,此時會向前提取前一條規則token if ((find = Expr.find[type])) { // Search, expanding context for leading sibling combinators // 嘗試一下能否通過這個搜索器搜到符合條件的初始集合seed if ((seed = find( token.matches[0].replace(runescape, funescape), rsibling.test(tokens[0].type) && context.parentNode || context ))) { //如果真的搜到了 // If seed is empty or no tokens remain, we can return early //把最后一條規則去除掉 tokens.splice(i, 1); selector = seed.length && toSelector(tokens); //看看當前剩余的選擇器是否為空 if (!selector) { //是的話,提前返回結果了。 push.apply(results, seed); return results; } //已經找到了符合條件的seed集合,此時前邊還有其他規則,跳出去 break; } } } } } // "div > p + div.aaron [type="checkbox"]" // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above // 交由compile來生成一個稱為終極匹配器 // 通過這個匹配器過濾seed,把符合條件的結果放到results里邊 // // //生成編譯函數 // var superMatcher = compile( selector, match ) // // //執行 // superMatcher(seed,context,!documentIsHTML,results,rsibling.test( selector )) // compile(selector, match)( seed, context, !documentIsHTML, results, rsibling.test(selector) ); return results; }
這個過程在簡單總結一下:
selector:"div > p + div.aaron input[type="checkbox"]"
解析規則:
1 按照從右到左
2 取出最后一個token 比如[type="checkbox"]
{
matches : Array[3]
type : "ATTR"
value : "[type="
checkbox "]"
}
3 過濾類型 如果type是 > + ~ 空 四種關系選擇器中的一種,則跳過,在繼續過濾
4 直到匹配到為 ID,CLASS,TAG 中一種 , 因為這樣才能通過瀏覽器的接口索取
5 此時seed種子合集中就有值了,這樣把刷選的條件給縮的很小了
6 如果匹配的seed的合集有多個就需要進一步的過濾了,修正選擇器 selector: "div > p + div.aaron [type="checkbox"]"
7 OK,跳到一下階段的編譯函數
Sizzle不僅僅是簡簡單單的從右往左匹配的
Sizzle1.8開始引入編譯函數的概念,也是下一章的重點