CSS的原理,如何解析?

智雲程式設計發表於2019-02-27

我們每天都在與CSS打交道,那麼CSS的原理是什麼呢?10個人裡面9個人都不能準確地答出來吧!

一、瀏覽器渲染

首先來回顧一下瀏覽器的渲染過程,先上圖:

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 語法檔案中自動生成解析器。
Webkit 使用 Flex 和 Bison 解析生成器從 CSS 語法檔案中自動生成解析器。

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

CSS的原理,如何解析?

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

CSS 的一些解析功能的入口也在此處,它們會呼叫 lex , parse 等生成程式碼。相對的,生成程式碼中需要的 CallBack 也需要在這裡實現。

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

CSSRule* CSSParser::createStyleRule(CSSSelector* selector)  
{  
    CSSStyleRule* rule = 0;  
    if (selector) {  
        rule = new CSSStyleRule(styleElement);  
        m_parsedStyleObjects.append(rule);  
        rule->setSelector(sinkFloatingSelector(selector));  
        rule->setDeclaration(new CSSMutableStyleDeclaration(rule, parsedProperties, numParsedProperties));  
    }  
    clearProperties();  
    return rule;  
}

從該函式的實現可以很清楚的看到,解析器達到某條件需要建立一個 CSSStyleRule 的時候將呼叫該函式,該函式的功能是建立一個 CSSStyleRule ,並將其新增已解析的樣式物件列表 m_parsedStyleObjects 中去,這裡的物件就是指的 Rule 。

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

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

1.透過呼叫 CSSStyleSheet 的 parseString 函式,將上述 CSS 解析過程啟動,解析完一遍後,把 Rule 都儲存在對應的 CSSStyleSheet 物件中;

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

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

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

三、CSS 選擇器解析順序

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

1.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 的元素。

2.在建立 Render Tree 時(WebKit 中的「Attachment」過程),瀏覽器就要為每個 DOM Tree 中的元素根據 CSS 的解析結果(Style Rules)來確定生成怎樣的 renderer。對於每個 DOM 元素,必須在所有 Style Rules 中找到符合的 selector 並將對應的規則進行合併。選擇器的「解析」實際是在這裡執行的,在遍歷 DOM Tree 時,從 Style Rules 中去尋找對應的 selector。

3.因為所有樣式規則可能數量很大,而且絕大多數不會匹配到當前的 DOM 元素(因為數量很大所以一般會建立規則索引樹),所以有一個快速的方法來判斷「這個 selector 不匹配當前元素」就是極其重要的。

4.如果正向解析,例如「div div p em」,我們首先就要檢查當前元素到 html 的整條路徑,找到最上層的 div,再往下找,如果遇到不匹配就必須回到最上層那個 div,往下再去匹配選擇器中的第一個 div,回溯若干次才能確定匹配與否,效率很低。

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

<div>
   <div class="jartto">
      <p>span> 111 span><p>
      <p>span> 222 span><p>
      <p><span> 333 <span><p>
      <p><span class='yellow'> 444 <span><p>
   <div>
<div>

CSS 選擇器:

div > div.jartto p span.yellow{
   color:yellow;
}

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

1.先找到所有 div 節點;
2.在 div 節點內找到所有的子 div ,並且是 class = “jartto”;
3.然後再依次匹配 p span.yellow 等情況;
4.遇到不匹配的情況,就必須回溯到一開始搜尋的 div 或者 p 節點,然後去搜尋下個節點,重複這樣的過程。

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

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

1.首先就查詢到 的元素;
2.緊接著我們判斷這些節點中的前兄弟節點是否符合 P 這個規則,這樣就又減少了集合的元素,只有符合當前的子規則才會匹配再上一條子規則。

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

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

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

四、CSS 語法解析過程

CSS 樣式表解析過程中講解的很細緻,這裡我們只看 CSS 語法直譯器,大致過程如下:
1.先建立 CSSStyleSheet 物件。將 CSSStyleSheet 物件的指標儲存到 CSSParser 物件中。
2.CSSParser 識別出一個 simple-selector ,形如 “div” 或者 “.class”。建立一個 CSSParserSelector 物件。
3.CSSParser 識別出一個關係符和另一個 simple-selecotr ,那麼修改之前建立的 simple-selecotr, 建立組合關係符。
4.迴圈第3步直至碰到逗號或者左大括號。
5.如果碰到逗號,那麼取出 CSSParser 的 reuse vector,然後將堆疊尾部的 CSSParserSelector 物件彈出存入 Vecotr 中,最後跳轉至第2步。如果碰到左大括號,那麼跳轉至第6步。
6.識別屬性名稱,將屬性名稱的 hash 值壓入直譯器堆疊。
7.識別屬性值,建立 CSSParserValue 物件,並將 CSSParserValue 物件存入直譯器堆疊。
8.將屬性名稱和屬性值彈出棧,建立 CSSProperty 物件。並將 CSSProperty 物件存入 CSSParser 成員變數m_parsedProperties 中。
9.如果識別處屬性名稱,那麼轉至第6步。如果識別右大括號,那麼轉至第10步。
10.將 reuse vector 從堆疊中彈出,並建立 CSSStyleRule 物件。CSSStyleRule 物件的選擇符就是 reuse vector, 樣式值就是 CSSParser 的成員變數 m_parsedProperties 。
11.把 CSSStyleRule 新增到 CSSStyleSheet 中。
12.清空 CSSParser 內部快取結果。
13.如果沒有內容了,那麼結束。否則跳轉值第2步。

五、內聯樣式如何解析?

透過上文的瞭解,我們知道,當 CSS Parser 解析完 CSS 指令碼後,會生成 CSSStyleSheetList ,他儲存在Document 物件上。為了更快的計算樣式,必須對這些 CSSStyleSheetList 進行重新組織。

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

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

可能類似如下的表現:

p > a { 
  color : red; 
  background-color:black;
}  
a {
  color : yellow
}  
div { 
  margin : 1px;
}

重新組織之後的陣列資料為(weight我只是表示了他們之間的相對大小,並非實際值。)

CSS的原理,如何解析?

好了,到這裡,我們來解決上述問題:
首先,要明確,內斂樣式只是 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屬性相等,他們也不共享;

span>p style="color:red">paragraph1span>p>
span>p style="color:red">paragraph2span>p>

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

深入瞭解,請參考:Webkit CSS 引擎分析 - 高效執行的 CSS 指令碼

自己是從事了五年的前端工程師,不少人私下問我,2019年前端該怎麼學,方法有沒有?

如果你依然在程式設計的世界裡迷茫,不知道自己的未來規劃,可以加入web前端學習交流群:731771211 裡面可以與大神一起交流並走出迷茫。新手可進群免費領取學習資料,看看前輩們是如何在程式設計的世界裡傲然前行!群裡不停更新最新的教程和學習方法(進群送web前端系統學習路線,詳細的前端專案實戰教學影片),有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入

點選: 加入


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901074/viewspace-2637131/,如需轉載,請註明出處,否則將追究法律責任。

相關文章