聲明:本文爲原創文章,如需轉載,請註明來源並保留原文連接Aaron,謝謝!javascript
因此咱們知道瀏覽器最終會將HTML文檔(或者說頁面)解析成一棵DOM樹,以下代碼將會翻譯成如下的DOM樹。css
<div id="text"> <p> <input type="text" /> </p> <div class="aaron"> <input type="checkbox" name="readme" /> <p>Sizzle</p> </div> </div>
若是想要操做到當中那個checkbox,咱們須要有一種表述方式,使得經過這個表達式讓瀏覽器知道咱們是想要操做哪一個DOM節點。
這個表述方式就是CSS選擇器,它是這樣表示的:div > p + .aaron input[type="checkbox"]
表達的意思是,div底下的p的兄弟節點,該節點的class爲aaron 而且後代中有一個元素是input其屬性type爲checkbox的。html
常見的選擇器:java
其實最終都是經過瀏覽器提供的接口實現的算法
獲取id爲test的DOM節點 數組
document.getElementById(「test」)
獲取節點名爲input的DOM節點瀏覽器
document.getElementsByTagName(「input」)
獲取屬性name爲checkbox的DOM節點緩存
document.getElementsByName(「checkbox」)
高級的瀏覽器還提供async
document.getElementsByClassName函數
document.querySelector
document.querySelectorAll
因爲低級瀏覽器並未提供這些高級點的接口,因此纔有了Sizzle這個CSS選擇器引擎。Sizzle引擎提供的接口跟document.querySelectorAll是同樣的,其輸入是一串選擇器字符串,輸出則是一個符合這個選擇器規則的DOM節點列表,所以第一步驟是要分析這個輸入的選擇器。
看看實際效果
window.onload = function() { console.log( Sizzle('div > div.Aaron p span.red') ) console.log( document.querySelectorAll('div > div.Aaron p span.red') ) }
在開始前,咱們必須瞭解一個真相:爲何排版引擎解析 CSS 選擇器時必定要從右往左解析?
簡單的來講瀏覽器從右到左進行查找的好處是爲了儘早過濾掉一些無關的樣式規則和元素
例如:
<title>aQuery</title> <script src="sizzle.js"></script> <script src="core.js"></script> <style> div > div.Aaron p span.red{ color:red; } </style> <div> <div class="Aaron"> <p><span>s1</span></p> <p><span>s2</span></p> <p><span>s3</span></p> <p><span class='red'>s4</span></p> </div> </div>
CSS選擇器:
div > div.Aaron p span.red
而若是按從左到右的方式進行查找:
1. 先找到全部div節點
2. 第一個div節點內找到全部的子div,而且是class=」Aaron」
3. 而後再一次匹配p span.red等狀況
4. 遇到不匹配的狀況,就必須回溯到一開始搜索的div或者p節點,而後去搜索下個節點,重複這樣的過程。這樣的搜索過程對於一個只是匹配不多節點的選擇器來講,效率是極低的,由於咱們花費了大量的時間在回溯匹配不符合規則的節點。
若是換個思路,咱們一開始過濾出跟目標節點最符合的集合出來,再在這個集合進行搜索,大大下降了搜索空間
從右到左來解析選擇器:
則首先就查找到<span class='red'>的元素。
firefox稱這種查找方式爲key selector(關鍵字查詢),所謂的關鍵字就是樣式規則中最後(最右邊)的規則,上面的key就是span.red。
緊接着咱們判斷這些節點中的前兄弟節點是否符合p這個規則,這樣就又減小了集合的元素,只有符合當前的子規則纔會匹配再上一條子規則
要知道DOM樹是一個什麼樣的結構,一個元素可能有若干子元素,若是每個都去判斷一下顯然性能太差。而一個子元素只有一個父元素,因此找起來很是方便。你能夠看看css的選擇器的設計,徹底是爲了優化從子元素找父元素而決定的。
打個好比 p span.showing
你認爲從一個p元素下面找到全部的span元素並判斷是否有class showing快,仍是找到全部的span元素判斷是否有class showing而且包括一個p父元素快 ?
因此瀏覽器解析CSS的引擎就是用這樣的算法去解析
關於解析機制
就拿javascript而言,解析過程能夠分爲預編譯與執行兩個階段,具體這裏不說多,可是有一個重要的點
在預編譯的時候經過詞法分析器與語法分期器的規則處理
在詞法分析過程當中,js解析器要下把腳本代碼的字符流轉換成記號流
好比:
a=(b-c);
解析後轉換成:
NAME "a" EQUALS OPEN_PARENTHESIS NAME "b" MINUS NAME "c" CLOSE_PARENTHESIS SEMICOLON
把代碼解析成Token的階段在編譯階段裏邊稱爲詞法分析
代碼通過詞法分析後就獲得了一個Token序列,緊接着拿Token序列去其餘事情
大概就是這個意思,在JS征途這本書看的,沒有研究V8過引擎,反正你們有興趣去看看書吧
這裏只想引伸出一個思想:
CSS選擇器其實也就是一段字符串,咱們須要分析出這個字符串背後對應的規則,在這裏Sizzle用了簡單的詞法分析。
因此在Sizzle中專門有一個tokenize處理器幹這個事情
咱們簡單的看看處理後的結果:
選擇器
selector: "div > div.Aaron p span.red"
通過tokenize處理器處理事後分解爲
一個數組對象,展開後
其實就是對每個標記都作了分解了
Sizzle的Token格式以下 :
Token:{ value:'匹配到的字符串', type:'對應的Token類型', matches:'正則匹配到的一個結構' }
這樣拿到匹配後的結構Token就去幹別的相關處理了!
看看整個源碼的解析:
//假設傳入進來的選擇器是:div > p + .aaron[type="checkbox"], #id:first-child //這裏能夠分爲兩個規則:div > p + .aaron[type="checkbox"] 以及 #id:first-child //返回的須要是一個Token序列 //Sizzle的Token格式以下 :{value:'匹配到的字符串', type:'對應的Token類型', matches:'正則匹配到的一個結構'} function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; //這裏的soFar是表示目前還未分析的字符串剩餘部分 //groups表示目前已經匹配到的規則組,在這個例子裏邊,groups的長度最後是2,存放的是每一個規則對應的Token序列 //若是cache裏邊有,直接拿出來便可 if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } //初始化 soFar = selector; groups = []; //這是最後要返回的結果,一個二維數組 //好比"title,div > :nth-child(even)"解析下面的符號流 // [ [{value:"title",type:"TAG",matches:["title"]}], // [{value:"div",type:["TAG",matches:["div"]}, // {value:">", type: ">"}, // {value:":nth-child(even)",type:"CHILD",matches:["nth", // "child","even",2,0,undefined,undefined,undefined]} // ] // ] //有多少個並聯選擇器,裏面就有多少個數組,數組裏面是擁有value與type的對象 //這裏的預處理器爲了對匹配到的Token適當作一些調整 //自行查看源碼,其實就是正則匹配到的內容的一個預處理 preFilters = Expr.preFilter; //遞歸檢測字符串 //好比"div > p + .aaron input[type="checkbox"]" while ( soFar ) { // Comma and first run // 以第一個逗號切割選擇符,而後去掉前面的部分 if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { //若是匹配到逗號 // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } //往規則組裏邊壓入一個Token序列,目前Token序列仍是空的 groups.push( tokens = [] ); } matched = false; // Combinators //將剛纔前面的部分以關係選擇器再進行劃分 //先處理這幾個特殊的Token : >, +, 空格, ~ //由於他們比較簡單,而且是單字符的 if ( (match = rcombinators.exec( soFar )) ) { //獲取到匹配的字符 matched = match.shift(); //放入Token序列中 tokens.push({ value: matched, // Cast descendant combinators to space type: match[0].replace( rtrim, " " ) }); //剩餘還未分析的字符串須要減去這段已經分析過的 soFar = soFar.slice( matched.length ); } // Filters //這裏開始分析這幾種Token : TAG, ID, CLASS, ATTR, CHILD, PSEUDO, NAME //將每一個選擇器組依次用ID,TAG,CLASS,ATTR,CHILD,PSEUDO這些正則進行匹配 //Expr.filter裏邊對應地 就有這些key /** * * *matchExpr 過濾正則 ATTR: /^\[[\x20\t\r\n\f]*((?:\\.|[\w-]|[^\x00-\xa0])+)[\x20\t\r\n\f]*(?:([*^$|!~]?=)[\x20\t\r\n\f]*(?:(['"])((?:\\.|[^\\])*?)\3|((?:\\.|[\w#-]|[^\x00-\xa0])+)|)|)[\x20\t\r\n\f]*\]/ CHILD: /^:(only|first|last|nth|nth-last)-(child|of-type)(?:\([\x20\t\r\n\f]*(even|odd|(([+-]|)(\d*)n|)[\x20\t\r\n\f]*(?:([+-]|)[\x20\t\r\n\f]*(\d+)|))[\x20\t\r\n\f]*\)|)/i CLASS: /^\.((?:\\.|[\w-]|[^\x00-\xa0])+)/ ID: /^#((?:\\.|[\w-]|[^\x00-\xa0])+)/ PSEUDO: /^:((?:\\.|[\w-]|[^\x00-\xa0])+)(?:\(((['"])((?:\\.|[^\\])*?)\3|((?:\\.|[^\\()[\]]|\[[\x20\t\r\n\f]*((?:\\.|[\w-]|[^\x00-\xa0])+)[\x20\t\r\n\f]*(?:([*^$|!~]?=)[\x20\t\r\n\f]*(?:(['"])((?:\\.|[^\\])*?)\8|((?:\\.|[\w#-]|[^\x00-\xa0])+)|)|)[\x20\t\r\n\f]*\])*)|.*)\)|)/ TAG: /^((?:\\.|[\w*-]|[^\x00-\xa0])+)/ bool: /^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$/i needsContext: /^[\x20\t\r\n\f]*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\([\x20\t\r\n\f]*((?:-\d)?\d*)[\x20\t\r\n\f]*\)|)(?=[^-]|$)/i * */ //若是經過正則匹配到了Token格式:match = matchExpr[ type ].exec( soFar ) //而後看看需不須要預處理:!preFilters[ type ] //若是須要 ,那麼經過預處理器將匹配到的處理一下 : match = preFilters[ type ]( match ) for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { matched = match.shift(); //放入Token序列中 tokens.push({ value: matched, type: type, matches: match }); //剩餘還未分析的字符串須要減去這段已經分析過的 soFar = soFar.slice( matched.length ); } } //若是到了這裏都還沒matched到,那麼說明這個選擇器在這裏有錯誤 //直接中斷詞法分析過程 //這就是Sizzle對詞法分析的異常處理 if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens //放到tokenCache函數裏進行緩存 //若是隻須要這個接口檢查選擇器的合法性,直接就返回soFar的剩餘長度,假若是大於零,說明選擇器不合法 //其他狀況,若是soFar長度大於零,拋出異常;不然把groups記錄在cache裏邊並返回, return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }
這裏要提出幾點:
好比解析的規則
div > p + .aaron[type="checkbox"], #id:first-child
1:groups收集並聯關係的處理
div > p + .aaron[type="checkbox"], #id:first-child
分解成
groups:[
0:div > p + .aaron[type="checkbox"],
1:#id:first-child
]
而後往下仍是會細分的
看看匹配第一個逗號切割選擇符,而後去掉前面的部分
match = rcomma.exec( soFar )
//並聯選擇器的正則 // /^[\x20\t\r\n\f]*,[\x20\t\r\n\f]*/ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
科普一下:
空白符正則:
whitespace = [\x20\t\r\n\f]
\xnn 由十六進制數nn指定的拉丁字符,如,\x0A等價於\n;
\uxxxx 由十六進制數xxxx指定的Unicode字符,例如\u0009等價於\t;
因此上面:
\x20 化爲二進制數爲 0010 0000;
ASCII碼錶 http://ascii.911cha.com/
字符編碼筆記 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
\t 製表符;
\r 回車;
\n 換行;
\f 換頁;
Sizzle這麼多正則關係,我就不信是直接寫出來的,呵呵
2:過濾簡單的單字符,幾個特殊的Token : >, +, 空格, ~
放入Token序列中,而後踢掉soFar中處理的字符
3: 將每一個選擇器組依次用ID,TAG,CLASS,ATTR,CHILD,PSEUDO這些正則進行匹配
經過遞歸soFar 其實就是 selector = div > p + .aaron[type="checkbox"], #id:first-child
matchExpr就定義了匹配規則
4: tokenCache( selector, groups ).slice( 0 );
緩存到tokenCache 詞法分析階段須要的緩存器
畫一張直觀圖便於理解