每天都用CSS,你真的懂CSS嗎?

做爲前端,咱們天天都在與CSS打交道,那麼CSS的原理是什麼呢?

1、瀏覽器渲染

開篇,咱們仍是不厭其煩的回顧一下瀏覽器的渲染過程,先上圖:
css

正如上圖所展現的,咱們瀏覽器渲染過程分爲了兩條主線:html

其一,HTML Parser 生成的 DOM 樹;
其二,CSS Parser 生成的 Style Rules ;前端

在這以後,DOM 樹與 Style Rules 會生成一個新的對象,也就是咱們常說的 Render Tree 渲染樹,結合 Layout 繪製在屏幕上,從而展示出來。node

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

2、Webkit CSS 解析器

瀏覽器 CSS 模塊負責 CSS 腳本解析,併爲每一個 Element 計算出樣式。CSS 模塊雖小,可是計算量大,設計很差每每成爲瀏覽器性能的瓶頸。
web

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

先來看一張圖:數組

Webkit 使用 Flex 和 Bison 解析生成器從 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

可是咱們解析所要的結果是什麼?

一、經過調用 CSSStyleSheet 的 parseString 函數,將上述 CSS 解析過程啓動,解析完一遍後,把 Rule 都存儲在對應的 CSSStyleSheet 對象中;

二、因爲目前規則依然是不易於處理的,還須要將之轉換成 CSSRuleSet。也就是將全部的純樣式規則存儲在對應的集合當中,這種集合的抽象就是 CSSRuleSet;

三、CSSRuleSet 提供了一個 addRulesFromSheet 方法,能將 CSSStyleSheet 中的 rule 轉換爲 CSSRuleSet 中的 rule ;

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

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

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

<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;
}

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

一、先找到全部 div 節點;
二、在 div 節點內找到全部的子 div ,而且是 class = 「jartto」;
三、而後再依次匹配 p span.yellow 等狀況;
四、遇到不匹配的狀況,就必須回溯到一開始搜索的 div 或者 p 節點,而後去搜索下個節點,重複這樣的過程。

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

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

一、首先就查找到 的元素;
二、緊接着咱們判斷這些節點中的前兄弟節點是否符合 P 這個規則,這樣就又減小了集合的元素,只有符合當前的子規則纔會匹配再上一條子規則。

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

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

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

4、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 內部緩存結果。
1三、若是沒有內容了,那麼結束。不然跳轉值第2步。

5、內聯樣式如何解析?

經過上文的瞭解,咱們知道,當 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我只是表示了他們之間的相對大小,並不是實際值。)

selector selector weight
a color:yellow 1
p > a color:red 2
p > a background-color:black 2
div margin:1px 3

好了,到這裏,咱們來解決上述問題:
首先,要明確,內斂樣式只是 CSS 三種加載方式之一;
其次,瀏覽器解析分爲兩個分支,HTML Parser 和 CSS Parser,兩個 Parser 各司其職,各盡其責;
最後,不一樣的 CSS 加載方式產生的 Style rule ,經過權重來肯定誰覆蓋誰;

到這裏就不難理解了,對瀏覽器來講,內聯樣式與其餘的加載樣式方式惟一的區別就是權重不一樣。

深刻了解,請閱讀Webkit CSS引擎分析https://blog.csdn.net/scusyq/article/details/7059063

6、何謂 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 腳本https://blog.csdn.net/scusyq/article/details/7059063

7、眼見爲實


如上圖,咱們能夠看到不一樣的 CSS 選擇器的組合,解析速度也會受到不一樣的影響,你還會輕視 CSS 解析原理嗎?

感興趣的同窗能夠參考這裏:speed/validity selectors test for frameworks

8、有何收穫?

一、使用 id selector 很是的高效。在使用 id selector 的時候須要注意一點:由於 id 是惟一的,因此不須要既指定 id 又指定 tagName:

Bad
p#id1 {color:red;}  
Good  
#id1 {color:red;}
固然,你非要這麼寫也沒有什麼問題,但這會增長 CSS 編譯與解析時間,實在是不值當。

二、避免深層次的 node ,譬如:

Bad  
div > div > div > p {color:red;} 
Good  
p-class{color:red;}

三、慎用 ChildSelector ;

四、不到萬不得已,不要使用 attribute selector,如:p[att1=」val1」]。這樣的匹配很是慢。更不要這樣寫:p[id=」id1」]。這樣將 id selector 退化成 attribute selector。

Bad  

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

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

Good 

#id1{color:red;}  

.class1{color:red;}

五、理解依賴繼承,若是某些屬性能夠繼承,那麼天然沒有必要在寫一遍;
六、規範真的很重要,不只僅是可讀性,也許會影響你的頁面性能。這裏推薦一個 CSS 規範http://nec.netease.com/standard/css-sort.html,能夠參考一下。

9、總結

「學會使用」永遠都是最基本的標準,可是懂得原理,你才能舉一反三,超越自我。

源自:https://juejin.im/post/5e09bb3e6fb9a0162a0b8b76

聲明:文章著做權歸做者全部,若有侵權,請聯繫小編刪除。

感謝 · 轉發歡迎你們留言

本文分享自微信公衆號 - web前端學習圈(web-xxq)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索