jQuery 2.0.3 源碼分析Sizzle引擎 - 詞法解析

聲明:本文爲原創文章,如需轉載,請註明來源並保留原文連接Aaron,謝謝!javascript

  • 瀏覽器從下載文檔到顯示頁面的過程是個複雜的過程,這裏包含了重繪和重排。各家瀏覽器引擎的工做原理略有差異,但也有必定規則。
  • 簡單講,一般在文檔初次加載時,瀏覽器引擎會解析HTML文檔來構建DOM樹,以後根據DOM元素的幾何屬性構建一棵用於渲染的樹。渲染樹的每一個節點都有大小和邊距等屬性,相似於盒子模型(因爲隱藏元素不須要顯示,渲染樹中並不包含DOM樹中隱藏的元素)。
  • 當渲染樹構建完成後,瀏覽器就能夠將元素放置到正確的位置了,再根據渲染樹節點的樣式屬性繪製出頁面。因爲瀏覽器的流佈局,對渲染樹的計算一般只須要遍歷一次就能夠完成

因此咱們知道瀏覽器最終會將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

  • #test表示id爲test的DOM節點
  • .aaron 表示class爲aaron的DOM節點
  • input表示節點名爲input的DOM節點
  • div > p表示div底下的p的DOM節點
  • div + p表示div的兄弟DOM節點p

其實最終都是經過瀏覽器提供的接口實現的算法

獲取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')  )

}

image

 


在開始前,咱們必須瞭解一個真相爲何排版引擎解析 CSS 選擇器時必定要從右往左解析? 

參考:How browsers work

  • HTML 通過解析生成 DOM Tree(這個咱們比較熟悉);而在 CSS 解析完畢後,須要將解析的結果與 DOM Tree 的內容一塊兒進行分析創建一棵 Render Tree,最終用來進行繪圖。Render Tree 中的元素(WebKit 中稱爲「renderers」,Firefox 下爲「frames」)與 DOM 元素相對應,但非一一對應:一個 DOM 元素可能會對應多個 renderer,如文本折行後,不一樣的「行」會成爲 render tree 種不一樣的 renderer。也有的 DOM 元素被 Render Tree 徹底無視,好比 display:none 的元素。
  • 在創建 Render Tree 時(WebKit 中的「Attachment」過程),瀏覽器就要爲每一個 DOM Tree 中的元素根據 CSS 的解析結果(Style Rules)來肯定生成怎樣的 renderer。對於每一個 DOM 元素,必須在全部 Style Rules 中找到符合的 selector 並將對應的規則進行合併。選擇器的「解析」實際是在這裏執行的,在遍歷 DOM Tree 時,從 Style Rules 中去尋找對應的 selector。
  • 由於全部樣式規則可能數量很大,並且絕大多數不會匹配到當前的 DOM 元素(由於數量很大因此通常會創建規則索引樹),因此有一個快速的方法來判斷「這個 selector 不匹配當前元素」就是極其重要的。
  • 若是正向解析,例如「div div p em」,咱們首先就要檢查當前元素到 html 的整條路徑,找到最上層的 div,再往下找,若是遇到不匹配就必須回到最上層那個 div,往下再去匹配選擇器中的第一個 div,回溯若干次才能肯定匹配與否,效率很低。
  • 逆向匹配則不一樣,若是當前的 DOM 元素是 div,而不是 selector 最後的 em,那隻要一步就能排除。只有在匹配時,纔會不斷向上找父節點進行驗證。
  • 但由於匹配的狀況遠遠低於不匹配的狀況,因此逆向匹配帶來的優點是巨大的。同時咱們也可以看出,在選擇器結尾加上「*」就大大下降了這種優點,這也就是不少優化原則提到的儘可能避免在選擇器末尾添加通配符的緣由。

 簡單的來講瀏覽器從右到左進行查找的好處是爲了儘早過濾掉一些無關的樣式規則和元素

例如:

    <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處理器處理事後分解爲

image

一個數組對象,展開後

image

其實就是對每個標記都作了分解了

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

image

   \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 詞法分析階段須要的緩存器

 

畫一張直觀圖便於理解

image

相關文章
相關標籤/搜索