天天都在使用CSS凶硅,那么CSS的原理是什么呢?

作為前端扫皱,我們每天都在與CSS打交道足绅,那么CSS的原理是什么呢?

一韩脑、瀏覽器渲染

開篇氢妈,我們還是不厭其煩的回顧一下瀏覽器的渲染過程,先上圖:


webkit render

正如上圖所展示的扰才,我們?yōu)g覽器渲染過程分為了兩條主線:
其一允懂,HTML Parser 生成的 DOM 樹;
其二衩匣,CSS Parser 生成的 Style Rules 蕾总;

在這之后,DOM 樹與 Style Rules 會(huì)生成一個(gè)新的對(duì)象琅捏,也就是我們常說的 Render Tree 渲染樹生百,結(jié)合 Layout 繪制在屏幕上,從而展現(xiàn)出來柄延。

本文的重點(diǎn)也就集中在第二條分支上蚀浆,我們來探究一下 CSS 解析原理缀程。

二、Webkit CSS 解析器

瀏覽器 CSS 模塊負(fù)責(zé) CSS 腳本解析市俊,并為每個(gè) Element 計(jì)算出樣式杨凑。CSS 模塊雖小,但是計(jì)算量大摆昧,設(shè)計(jì)不好往往成為瀏覽器性能的瓶頸撩满。

CSS 模塊在實(shí)現(xiàn)上有幾個(gè)特點(diǎn):CSS 對(duì)象眾多(顆粒小而多),計(jì)算頻繁(為每個(gè) Element 計(jì)算樣式)绅你。這些特性決定了 webkit 在實(shí)現(xiàn) CSS 引擎上采取的設(shè)計(jì)伺帘,算法。如何高效的計(jì)算樣式是瀏覽器內(nèi)核的重點(diǎn)也是難點(diǎn)忌锯。

先來看一張圖:


webkit css parse

Webkit 使用 Flex 和 Bison 解析生成器從 CSS 語(yǔ)法文件中自動(dòng)生成解析器伪嫁。

它們都是將每個(gè) CSS 文件解析為樣式表對(duì)象,每個(gè)對(duì)象包含 CSS 規(guī)則偶垮,CSS 規(guī)則對(duì)象包含選擇器和聲明對(duì)象张咳,以及其他一些符合 CSS 語(yǔ)法的對(duì)象,下圖可能會(huì)比較明了:

css rule

Webkit 使用了自動(dòng)代碼生成工具生成了相應(yīng)的代碼针史,也就是說詞法分析語(yǔ)法分析這部分代碼是自動(dòng)生成的晶伦,而 Webkit 中實(shí)現(xiàn)的 CallBack 函數(shù)就是在 CSSParser 中碟狞。

CSS 的一些解析功能的入口也在此處啄枕,它們會(huì)調(diào)用 lex , parse 等生成代碼。相對(duì)的族沃,生成代碼中需要的 CallBack 也需要在這里實(shí)現(xiàn)频祝。

舉例來說,現(xiàn)在我們來看其中一個(gè)回調(diào)函數(shù)的實(shí)現(xiàn)脆淹,createStyleRule(),該函數(shù)將在一般性的規(guī)則需要被建立的時(shí)候調(diào)用常空,代碼如下:

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

從該函數(shù)的實(shí)現(xiàn)可以很清楚的看到,解析器達(dá)到某條件需要?jiǎng)?chuàng)建一個(gè) CSSStyleRule 的時(shí)候?qū)⒄{(diào)用該函數(shù)盖溺,該函數(shù)的功能是創(chuàng)建一個(gè) CSSStyleRule 漓糙,并將其添加已解析的樣式對(duì)象列表 m_parsedStyleObjects 中去,這里的對(duì)象就是指的 Rule 烘嘱。

那么如此一來昆禽,經(jīng)過這樣一番解析后,作為輸入的樣式表中的所有 Style Rule 將被轉(zhuǎn)化為 Webkit 的內(nèi)部模型對(duì)象 CSSStyleRule 對(duì)象蝇庭,存儲(chǔ)在 m_parsedStyleObjects 中醉鳖,它是一個(gè) Vector

但是我們解析所要的結(jié)果是什么哮内?

1.通過調(diào)用 CSSStyleSheet 的 parseString 函數(shù)盗棵,將上述 CSS 解析過程啟動(dòng),解析完一遍后,把 Rule 都存儲(chǔ)在對(duì)應(yīng)的 CSSStyleSheet 對(duì)象中纹因;

2.由于目前規(guī)則依然是不易于處理的喷屋,還需要將之轉(zhuǎn)換成 CSSRuleSet。也就是將所有的純樣式規(guī)則存儲(chǔ)在對(duì)應(yīng)的集合當(dāng)中瞭恰,這種集合的抽象就是 CSSRuleSet逼蒙;

3.CSSRuleSet 提供了一個(gè) addRulesFromSheet 方法,能將 CSSStyleSheet 中的 rule 轉(zhuǎn)換為 CSSRuleSet 中的 rule 寄疏;

4.基于這些個(gè) CSSRuleSet 來決定每個(gè)頁(yè)面中的元素的樣式是牢;

三、CSS 選擇器解析順序

可能很多同學(xué)都知道排版引擎解析 CSS 選擇器時(shí)是從右往左解析陕截,這是為什么呢驳棱?

1.HTML 經(jīng)過解析生成 DOM Tree(這個(gè)我們比較熟悉);而在 CSS 解析完畢后农曲,需要將解析的結(jié)果與 DOM Tree 的內(nèi)容一起進(jìn)行分析建立一棵 Render Tree社搅,最終用來進(jìn)行繪圖。Render Tree 中的元素(WebKit 中稱為「renderers」乳规,F(xiàn)irefox 下為「frames」)與 DOM 元素相對(duì)應(yīng)形葬,但非一一對(duì)應(yīng):一個(gè) DOM 元素可能會(huì)對(duì)應(yīng)多個(gè) renderer,如文本折行后暮的,不同的「行」會(huì)成為 render tree 種不同的 renderer笙以。也有的 DOM 元素被 Render Tree 完全無視,比如 display:none 的元素冻辩。

2.在建立 Render Tree 時(shí)(WebKit 中的「Attachment」過程)猖腕,瀏覽器就要為每個(gè) DOM Tree 中的元素根據(jù) CSS 的解析結(jié)果(Style Rules)來確定生成怎樣的 renderer。對(duì)于每個(gè) DOM 元素恨闪,必須在所有 Style Rules 中找到符合的 selector 并將對(duì)應(yīng)的規(guī)則進(jìn)行合并倘感。選擇器的「解析」實(shí)際是在這里執(zhí)行的,在遍歷 DOM Tree 時(shí)咙咽,從 Style Rules 中去尋找對(duì)應(yīng)的 selector老玛。

3.因?yàn)樗袠邮揭?guī)則可能數(shù)量很大,而且絕大多數(shù)不會(huì)匹配到當(dāng)前的 DOM 元素(因?yàn)閿?shù)量很大所以一般會(huì)建立規(guī)則索引樹)钧敞,所以有一個(gè)快速的方法來判斷「這個(gè) selector 不匹配當(dāng)前元素」就是極其重要的蜡豹。

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

對(duì)于上述描述洛搀,我們先有個(gè)大概的認(rèn)知。接下來我們來看這樣一個(gè)例子佑淀,參考地址

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

對(duì)于上述例子留美,如果按從左到右的方式進(jìn)行查找:
1.先找到所有 div 節(jié)點(diǎn);
2.在 div 節(jié)點(diǎn)內(nèi)找到所有的子 div ,并且是 class = “jartto”伸刃;
3.然后再依次匹配 p span.yellow 等情況谎砾;
4.遇到不匹配的情況,就必須回溯到一開始搜索的 div 或者 p 節(jié)點(diǎn)捧颅,然后去搜索下個(gè)節(jié)點(diǎn)景图,重復(fù)這樣的過程。

這樣的搜索過程對(duì)于一個(gè)只是匹配很少節(jié)點(diǎn)的選擇器來說碉哑,效率是極低的挚币,因?yàn)槲覀兓ㄙM(fèi)了大量的時(shí)間在回溯匹配不符合規(guī)則的節(jié)點(diǎn)。

如果換個(gè)思路扣典,我們一開始過濾出跟目標(biāo)節(jié)點(diǎn)最符合的集合出來妆毕,再在這個(gè)集合進(jìn)行搜索,大大降低了搜索空間贮尖。來看看從右到左來解析選擇器:
1.首先就查找到 的元素笛粘;
2.緊接著我們判斷這些節(jié)點(diǎn)中的前兄弟節(jié)點(diǎn)是否符合 P 這個(gè)規(guī)則,這樣就又減少了集合的元素远舅,只有符合當(dāng)前的子規(guī)則才會(huì)匹配再上一條子規(guī)則闰蛔。

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

試想一下随抠,如果采用從左至右的方式讀取 CSS 規(guī)則裁着,那么大多數(shù)規(guī)則讀到最后(最右)才會(huì)發(fā)現(xiàn)是不匹配的,這樣會(huì)做費(fèi)時(shí)耗能拱她,最后有很多都是無用的二驰;而如果采取從右向左的方式,那么只要發(fā)現(xiàn)最右邊選擇器不匹配秉沼,就可以直接舍棄了桶雀,避免了許多無效匹配矿酵。

瀏覽器 CSS 匹配核心算法的規(guī)則是以從右向左方式匹配節(jié)點(diǎn)的。這樣做是為了減少無效匹配次數(shù)矗积,從而匹配快全肮、性能更優(yōu)。

四棘捣、CSS 語(yǔ)法解析過程

CSS 樣式表解析過程中講解的很細(xì)致辜腺,這里我們只看 CSS 語(yǔ)法解釋器,大致過程如下:
1.先創(chuàng)建 CSSStyleSheet 對(duì)象乍恐。將 CSSStyleSheet 對(duì)象的指針存儲(chǔ)到 CSSParser 對(duì)象中评疗。
2.CSSParser 識(shí)別出一個(gè) simple-selector ,形如 “div” 或者 “.class”茵烈。創(chuàng)建一個(gè) CSSParserSelector 對(duì)象壤巷。
3.CSSParser 識(shí)別出一個(gè)關(guān)系符和另一個(gè) simple-selecotr ,那么修改之前創(chuàng)建的 simple-selecotr, 創(chuàng)建組合關(guān)系符瞧毙。
4.循環(huán)第3步直至碰到逗號(hào)或者左大括號(hào)胧华。
5.如果碰到逗號(hào),那么取出 CSSParser 的 reuse vector宙彪,然后將堆棧尾部的 CSSParserSelector 對(duì)象彈出存入 Vecotr 中矩动,最后跳轉(zhuǎn)至第2步。如果碰到左大括號(hào)释漆,那么跳轉(zhuǎn)至第6步悲没。
6.識(shí)別屬性名稱,將屬性名稱的 hash 值壓入解釋器堆棧男图。
7.識(shí)別屬性值示姿,創(chuàng)建 CSSParserValue 對(duì)象,并將 CSSParserValue 對(duì)象存入解釋器堆棧逊笆。
8.將屬性名稱和屬性值彈出棧栈戳,創(chuàng)建 CSSProperty 對(duì)象。并將 CSSProperty 對(duì)象存入 CSSParser 成員變量m_parsedProperties 中难裆。
9.如果識(shí)別處屬性名稱子檀,那么轉(zhuǎn)至第6步。如果識(shí)別右大括號(hào)乃戈,那么轉(zhuǎn)至第10步褂痰。
10.將 reuse vector 從堆棧中彈出,并創(chuàng)建 CSSStyleRule 對(duì)象症虑。CSSStyleRule 對(duì)象的選擇符就是 reuse vector, 樣式值就是 CSSParser 的成員變量 m_parsedProperties 缩歪。
11.把 CSSStyleRule 添加到 CSSStyleSheet 中。
12.清空 CSSParser 內(nèi)部緩存結(jié)果谍憔。
13.如果沒有內(nèi)容了匪蝙,那么結(jié)束主籍。否則跳轉(zhuǎn)值第2步。

五骗污、內(nèi)聯(lián)樣式如何解析崇猫?

通過上文的了解,我們知道需忿,當(dāng) CSS Parser 解析完 CSS 腳本后诅炉,會(huì)生成 CSSStyleSheetList ,他保存在Document 對(duì)象上屋厘。為了更快的計(jì)算樣式涕烧,必須對(duì)這些 CSSStyleSheetList 進(jìn)行重新組織。

計(jì)算樣式就是從 CSSStyleSheetList 中找出所有匹配相應(yīng)元素的 property-value 對(duì)汗洒。匹配會(huì)通過CSSSelector 來驗(yàn)證议纯,同時(shí)需要滿足層疊規(guī)則。

將所有的 declaration 中的 property 組織成一個(gè)大的數(shù)組溢谤。數(shù)組中的每一項(xiàng)紀(jì)錄了這個(gè) property 的selector瞻凤,property 的值,權(quán)重(層疊規(guī)則)世杀。

可能類似如下的表現(xiàn):

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

重新組織之后的數(shù)組數(shù)據(jù)為(weight我只是表示了他們之間的相對(duì)大小阀参,并非實(shí)際值。)

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

好了瞻坝,到這里蛛壳,我們來解決上述問題:
首先,要明確所刀,內(nèi)斂樣式只是 CSS 三種加載方式之一衙荐;
其次,瀏覽器解析分為兩個(gè)分支浮创,HTML Parser 和 CSS Parser忧吟,兩個(gè) Parser 各司其職,各盡其責(zé)蒸矛;
最后瀑罗,不同的 CSS 加載方式產(chǎn)生的 Style rule ,通過權(quán)重來確定誰(shuí)覆蓋誰(shuí)雏掠;

到這里就不難理解了,對(duì)瀏覽器來說劣像,內(nèi)聯(lián)樣式與其他的加載樣式方式唯一的區(qū)別就是權(quán)重不同乡话。

深入了解,請(qǐng)閱讀Webkit CSS引擎分析

六耳奕、何謂 computedStyle 绑青?

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

瀏覽器還有一個(gè)非常棒的策略闸婴,在特定情況下坏挠,瀏覽器會(huì)共享 computedStyle,網(wǎng)頁(yè)中能共享的標(biāo)簽非常多邪乍,所以能極大的提升執(zhí)行效率降狠!如果能共享,那就不需要執(zhí)行匹配算法了庇楞,執(zhí)行效率自然非常高榜配。

也就是說:如果兩個(gè)或多個(gè) element 的 computedStyle 不通過計(jì)算可以確認(rèn)他們相等,那么這些 computedStyle 相等的 elements 只會(huì)計(jì)算一次樣式吕晌,其余的僅僅共享該 computedStyle 蛋褥。

那么有哪些規(guī)則會(huì)共享 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>
    

當(dāng)然淫茵,知道了共享 computedStyle 的規(guī)則,那么反面我們也就了解了:不會(huì)共享 computedStyle 的規(guī)則屎蜓,這里就不展開討論了痘昌。

深入了解,請(qǐng)參考:Webkit CSS 引擎分析 - 高效執(zhí)行的 CSS 腳本

七炬转、眼見為實(shí)

parse speed

如上圖辆苔,我們可以看到不同的 CSS 選擇器的組合,解析速度也會(huì)受到不同的影響扼劈,你還會(huì)輕視 CSS 解析原理嗎驻啤?

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

八、有何收獲荐吵?

1.使用 id selector 非常的高效骑冗。在使用 id selector 的時(shí)候需要注意一點(diǎn):因?yàn)?id 是唯一的,所以不需要既指定 id 又指定 tagName:

Bad
p#id1 {color:red;}  
Good  
#id1 {color:red;}

當(dāng)然先煎,你非要這么寫也沒有什么問題贼涩,但這會(huì)增加 CSS 編譯與解析時(shí)間,實(shí)在是不值當(dāng)薯蝎。

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

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

3.慎用 ChildSelector ;

4.不到萬不得已占锯,不要使用 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;}

5.理解依賴?yán)^承,如果某些屬性可以繼承却紧,那么自然沒有必要在寫一遍桐臊;
6.規(guī)范真的很重要,不僅僅是可讀性啄寡,也許會(huì)影響你的頁(yè)面性能豪硅。這里推薦一個(gè) CSS 規(guī)范,可以參考一下挺物。

九懒浮、總結(jié)

“學(xué)會(huì)使用”永遠(yuǎn)都是最基本的標(biāo)準(zhǔn),但是懂得原理识藤,你才能觸類旁通砚著,超越自我。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痴昧,一起剝皮案震驚了整個(gè)濱河市稽穆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赶撰,老刑警劉巖舌镶,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異豪娜,居然都是意外死亡餐胀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門瘤载,熙熙樓的掌柜王于貴愁眉苦臉地迎上來否灾,“玉大人,你說我怎么就攤上這事鸣奔∧迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵挎狸,是天一觀的道長(zhǎng)扣汪。 經(jīng)常有香客問我,道長(zhǎng)锨匆,這世上最難降的妖魔是什么私痹? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮统刮,結(jié)果婚禮上紊遵,老公的妹妹穿的比我還像新娘。我一直安慰自己侥蒙,他們只是感情好暗膜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鞭衩,像睡著了一般学搜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上论衍,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天瑞佩,我揣著相機(jī)與錄音,去河邊找鬼坯台。 笑死炬丸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蜒蕾。 我是一名探鬼主播稠炬,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼咪啡!你這毒婦竟也來了首启?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤撤摸,失蹤者是張志新(化名)和其女友劉穎毅桃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體准夷,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钥飞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冕象。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片代承。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖渐扮,靈堂內(nèi)的尸體忽然破棺而出论悴,到底是詐尸還是另有隱情,我是刑警寧澤墓律,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布膀估,位于F島的核電站,受9級(jí)特大地震影響耻讽,放射性物質(zhì)發(fā)生泄漏察纯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饼记。 院中可真熱鬧香伴,春花似錦、人聲如沸具则。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)博肋。三九已至低斋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匪凡,已是汗流浹背膊畴。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留病游,地道東北人唇跨。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像礁遵,于是被迫代替她去往敵國(guó)和親轻绞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容