探究 CSS 解析原理


一、瀏覽器渲染

開篇,我們還是不厭其煩的回顧一下瀏覽器的渲染過程,

先上圖:


正如上圖所展示的,我們瀏覽器渲染過程分為了兩條主線:
其一,HTML Parser 生成的 DOM 樹;
其二,CSS Parser 生成的 Style Rules ;

在這之后,DOM 樹與 Style Rules 會生成一個新的對象,也就是我們常說的 Render Tree 渲染樹,結合 Layout 繪制在屏幕上,從而展現出來。

本文的重點也就集中在第二條分支上,我們來探究一下 CSS 解析原理。

二、Webkit CSS 解析器

瀏覽器 CSS 模塊負責 CSS 腳本解析,並為每個 Element 計算出樣式。CSS 模塊雖小,但是計算量大,設計不好往往成為瀏覽器性能的瓶頸。

CSS 模塊在實現上有幾個特點:CSS 對象眾多(顆粒小而多),計算頻繁(為每個 Element 計算樣式)。這些特性決定了 webkit 在實現 CSS 引擎上采取的設計,算法。如何高效的計算樣式是瀏覽器內核的重點也是難點。

先來看一張圖:



Webkit 使用 Flex 和 Bison 解析生成器從 CSS 語法文件中自動生成解析器。

它們都是將每個 CSS 文件解析為樣式表對象,每個對象包含 CSS 規則,CSS 規則對象包含選擇器和聲明對象,以及其他一些符合 CSS 語法的對象,下圖可能會比較明了:



Webkit 使用了自動代碼生成工具生成了相應的代碼,也就是說詞法分析和語法分析這部分代碼是自動生成的,而 Webkit 中實現的 CallBack 函數就是在 CSSParser 中。

CSS 的一些解析功能的入口也在此處,它們會調用 lex , parse 等生成代碼。相對的,生成代碼中需要的 CallBack 也需要在這里實現。

舉例來說,現在我們來看其中一個回調函數的實現,createStyleRule(),該函數將在一般性的規則需要被建立的時候調用,代碼如下:

      
      
      
              
  1. CSSRule* CSSParser::createStyleRule(CSSSelector* selector)  

  2. {  

  3.    CSSStyleRule* rule = 0;  

  4.    if (selector) {  

  5.        rule = new CSSStyleRule(styleElement);  

  6.        m_parsedStyleObjects.append(rule);  

  7.        rule->setSelector(sinkFloatingSelector(selector));  

  8.        rule->setDeclaration(new CSSMutableStyleDeclaration(rule, parsedProperties, numParsedProperties));  

  9.    }  

  10.    clearProperties();  

  11.    return rule;  

  12. }

從該函數的實現可以很清楚的看到,解析器達到某條件需要創建一個 CSSStyleRule 的時候將調用該函數,該函數的功能是創建一個 CSSStyleRule ,並將其添加已解析的樣式對象列表 m_parsedStyleObjects 中去,這里的對象就是指的 Rule 。

那么如此一來,經過這樣一番解析后,作為輸入的樣式表中的所有 Style Rule 將被轉化為 Webkit 的內部模型對象 CSSStyleRule 對象,存儲在 m_parsedStyleObjects 中,它是一個 Vector。

但是我們解析所要的結果是什么?

  • 通過調用 CSSStyleSheet 的 parseString 函數,將上述 CSS 解析過程啟動,解析完一遍后,把 Rule 都存儲在對應的 CSSStyleSheet 對象中;

  • 由於目前規則依然是不易於處理的,還需要將之轉換成 CSSRuleSet。也就是將所有的純樣式規則存儲在對應的集合當中,這種集合的抽象就是 CSSRuleSet;

  • CSSRuleSet 提供了一個 addRulesFromSheet 方法,能將 CSSStyleSheet 中的 rule 轉換為 CSSRuleSet 中的 rule ;

  • 基於這些個 CSSRuleSet 來決定每個頁面中的元素的樣式;

這里描述了大致過程,深入閱讀可以查看如下鏈接:

  • Webkit CSS 引擎分析

  • CSS 樣式表解析過程

  • Webkit CSS實現

三、CSS 選擇器解析順序

可能很多同學都知道排版引擎解析 CSS 選擇器時是從右往左解析,這是為什么呢?

  • 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,回溯若干次才能確定匹配與否,效率很低。

對於上述描述,我們先有個大概的認知。接下來我們來看這樣一個例子,參考地址:

      
      
      
              
  1. <div>

  2.   <div class="jartto">

  3.      <p><span> 111 </span></p>

  4.      <p><span> 222 </span></p>

  5.      <p><span> 333 </span></p>

  6.      <p><span class='yellow'> 444 </span></p>

  7.   </div>

  8. </div>

CSS 選擇器:

      
      
      
              
  1. div > div.jartto p span.yellow{

  2.   color:yellow;

  3. }

對於上述例子,如果按從左到右的方式進行查找:

  • 先找到所有 div 節點;

  • 在 div 節點內找到所有的子 div ,並且是 class = “jartto”;

  • 然后再依次匹配 p span.yellow 等情況;

  • 遇到不匹配的情況,就必須回溯到一開始搜索的 div 或者 p 節點,然后去搜索下個節點,重復這樣的過程。

這樣的搜索過程對於一個只是匹配很少節點的選擇器來說,效率是極低的,因為我們花費了大量的時間在回溯匹配不符合規則的節點。

如果換個思路,我們一開始過濾出跟目標節點最符合的集合出來,再在這個集合進行搜索,大大降低了搜索空間。來看看從右到左來解析選擇器:

  • 首先就查找到 的元素;

  • 緊接着我們判斷這些節點中的前兄弟節點是否符合 P 這個規則,這樣就又減少了集合的元素,只有符合當前的子規則才會匹配再上一條子規則。

結果顯而易見了,眾所周知,在 DOM 樹中一個元素可能有若干子元素,如果每一個都去判斷一下顯然性能太差。而一個子元素只有一個父元素,所以找起來非常方便。

試想一下,如果采用從左至右的方式讀取 CSS 規則,那么大多數規則讀到最后(最右)才會發現是不匹配的,這樣會做費時耗能,最后有很多都是無用的;而如果采取從右向左的方式,那么只要發現最右邊選擇器不匹配,就可以直接舍棄了,避免了許多無效匹配。

瀏覽器 CSS 匹配核心算法的規則是以從右向左方式匹配節點的。這樣做是為了減少無效匹配次數,從而匹配快、性能更優。

深入閱讀,請移步:

  • jQuery 源碼解析

  • CSS 選擇器從右向左的匹配規則

  • CSS 選擇器

四、CSS 語法解析過程

CSS 樣式表解析過程中講解的很細致,這里我們只看 CSS 語法解釋器,大致過程如下:

  • 先創建 CSSStyleSheet 對象。將 CSSStyleSheet 對象的指針存儲到 CSSParser 對象中。

  • CSSParser 識別出一個 simple-selector ,形如 “div” 或者 “.class”。創建一個 CSSParserSelector 對象。

  • CSSParser 識別出一個關系符和另一個 simple-selecotr ,那么修改之前創建的 simple-selecotr, 創建組合關系符。

  • 循環第3步直至碰到逗號或者左大括號。

  • 如果碰到逗號,那么取出 CSSParser 的 reuse vector,然后將堆棧尾部的 CSSParserSelector 對象彈出存入 Vecotr 中,最后跳轉至第2步。如果碰到左大括號,那么跳轉至第6步。

  • 識別屬性名稱,將屬性名稱的 hash 值壓入解釋器堆棧。

  • 識別屬性值,創建 CSSParserValue 對象,並將 CSSParserValue 對象存入解釋器堆棧。

  • 將屬性名稱和屬性值彈出棧,創建 CSSProperty 對象。並將 CSSProperty 對象存入 CSSParser 成員變量m_parsedProperties 中。

  • 如果識別處屬性名稱,那么轉至第6步。如果識別右大括號,那么轉至第10步。

  • 將 reuse vector 從堆棧中彈出,並創建 CSSStyleRule 對象。CSSStyleRule 對象的選擇符就是 reuse vector, 樣式值就是 CSSParser 的成員變量 m_parsedProperties 。

  • 把 CSSStyleRule 添加到 CSSStyleSheet 中。

  • 清空 CSSParser 內部緩存結果。

  • 如果沒有內容了,那么結束。否則跳轉值第2步。

五、內聯樣式如何解析?

通過上文的了解,我們知道,當 CSS Parser 解析完 CSS 腳本后,會生成 CSSStyleSheetList ,他保存在Document 對象上。為了更快的計算樣式,必須對這些 CSSStyleSheetList 進行重新組織。

計算樣式就是從 CSSStyleSheetList 中找出所有匹配相應元素的 property-value 對。匹配會通過CSSSelector 來驗證,同時需要滿足層疊規則。

將所有的 declaration 中的 property 組織成一個大的數組。數組中的每一項紀錄了這個 property 的selector,property 的值,權重(層疊規則)。

可能類似如下的表現:

      
      
      
              
  1. p > a {

  2.  color : red;

  3.  background-color:black;

  4. }  

  5. a {

  6.  color : yellow

  7. }  

  8. div {

  9.  margin : 1px;

  10. }

重新組織之后的數組數據為(weight我只是表示了他們之間的相對大小,並非實際值。)

      
      
      
              
  1. selector      property                       weight  

  2. a             color:yellow                   1  

  3. p > a         color:red                      2  

  4. p > a         background-color:black         2  

  5. div           margin:1px                     3

好了,到這里,我們來解決上述問題:
首先,要明確,內斂樣式只是 CSS 三種加載方式之一;

其次,瀏覽器解析分為兩個分支,HTML Parser 和 CSS Parser,兩個 Parser 各司其職,各盡其責;

最后,不同的 CSS 加載方式產生的 Style rule ,通過權重來確定誰覆蓋誰;

到這里就不難理解了,對瀏覽器來說,內聯樣式與其他的加載樣式方式唯一的區別就是權重不同。

深入了解,請閱讀Webkit CSS引擎分析

六、何謂 computedStyle ?

到這里,你以為完了?Too young too simple, sometimes naive!

瀏覽器還有一個非常棒的策略,在特定情況下,瀏覽器會共享 computedStyle,網頁中能共享的標簽非常多,所以能極大的提升執行效率!如果能共享,那就不需要執行匹配算法了,執行效率自然非常高。

也就是說:如果兩個或多個 element 的 computedStyle 不通過計算可以確認他們相等,那么這些 computedStyle 相等的 elements 只會計算一次樣式,其余的僅僅共享該 computedStyle 。

那么有哪些規則會共享 computedStyle 呢?

  • 該共享的element不能有id屬性且CSS中還有該id的StyleRule.哪怕該StyleRule與Element不匹配。

  • tagName和class屬性必須一樣;

  • mappedAttribute必須相等;

  • 不能使用sibling selector,譬如:first-child, :last-selector, + selector;

  • 不能有style屬性。哪怕style屬性相等,他們也不共享;

      
      
      
              
  1. <span><p style="color:red">paragraph1</span></p>

  2. <span><p style="color:red">paragraph2</span></p>

當然,知道了共享 computedStyle 的規則,那么反面我們也就了解了:不會共享 computedStyle 的規則,這里就不展開討論了。

深入了解,請參考:Webkit CSS 引擎分析 - 高效執行的 CSS 腳本

七、眼見為實

如上圖,我們可以看到不同的 CSS 選擇器的組合,解析速度也會受到不同的影響,你還會輕視 CSS 解析原理嗎?

感興趣的同學可以參考這里:speed/validity selectors test for frameworks

八、有何收獲?

1.使用 id selector 非常的高效。在使用 id selector 的時候需要注意一點:因為 id 是唯一的,所以不需要既指定 id 又指定 tagName:

      
      
      
              
  1. Bad

  2. p#id1 {color:red;}  


  3. Good  

  4. #id1 {color:red;}

當然,你非要這么寫也沒有什么問題,但這會增加 CSS 編譯與解析時間,實在是不值當

2.避免深層次的 node ,譬如:

      
      
      
              
  1. Bad  

  2. div > div > div > p {color:red;}


  3. Good  

  4. p-class{color:red;}

3.慎用 ChildSelector ;

4.不到萬不得已,不要使用 attribute selector,如:p[att1=”val1”]。這樣的匹配非常慢。更不要這樣寫:p[id=”id1”]。這樣將 id selector 退化成 attribute selector。

      
      
      
              
  1. Bad  

  2. p[id="id1"]{color:red;}  

  3. p[class="class1"]{color:red;}  


  4. Good

  5. #id1{color:red;}  

  6. .class1{color:red;}

5.理解依賴繼承,如果某些屬性可以繼承,那么自然沒有必要在寫一遍;

6.規范真的很重要,不僅僅是可讀性,也許會影響你的頁面性能。這里推薦一個 CSS 規范,可以參考一下。

九、總結

“學會使用”永遠都是最基本的標准,但是懂得原理,你才能觸類旁通,超越自我。

十、更多資源

  • CSS 解析順序

  • 優先級詳細探索

  • 簡單剖析 CSS 的解析規則


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM