1.瀏覽器的構(gòu)成
User Interface:
用戶界面,包括瀏覽器中可見的地址輸入框、瀏覽器前進(jìn)返回按鈕、書簽,歷史記錄等用戶可操作的功能選項(xiàng)。
Browser engine:
瀏覽器引擎,可以在用戶界面和渲染引擎之間傳送指令或在客戶端本地緩存中讀寫數(shù)據(jù),是瀏覽器各個(gè)部分之間相互通信的核心。
Rendering engine:
渲染引擎,解析 DOM 文檔和 CSS 規(guī)則并將內(nèi)容排版到瀏覽器中顯示有樣式的界面,也就是排版引擎,我們常說的瀏覽器內(nèi)核主要指的就是渲染引擎。
Networking:
網(wǎng)絡(luò)功能模塊,是瀏覽器開啟網(wǎng)絡(luò)線程發(fā)送請(qǐng)求以及下載資源的模塊。
JavaScript Interpreter:
JS 引擎,解釋和執(zhí)行 JS 腳本部分,例如 V8 引擎。
UI Backend:
UI 后端則是用于繪制基本的瀏覽器窗口內(nèi)控件,比如組合選擇框、按鈕、輸入框等。
Data Persistence:
數(shù)據(jù)持久化存儲(chǔ),涉及 Cookie、LocalStorage 等一些客戶端存儲(chǔ)技術(shù),可以通過瀏覽器引擎提供的 API 進(jìn)行調(diào)用。
2.渲染引擎
渲染引擎,解析 DOM 文檔和 CSS 規(guī)則并將內(nèi)容排版到瀏覽器中顯示有樣式的界面,也就是排版引擎,我們常說的瀏覽器內(nèi)核主要指的就是渲染引擎。
上圖中,我們需要關(guān)注兩條主線:
其一,HTML Parser 生成的 DOM 樹;
其二,CSS Parser 生成的 Style Rules(CSSOM 樹);
在這之后,DOM 樹與 Style Rules 會(huì)生成一個(gè)新的對(duì)象,也就是我們常說的 Render Tree渲染樹,結(jié)合 Layout 繪制在屏幕上,從而展現(xiàn)出來。
3.CSS 特性
1.優(yōu)先級(jí)
選擇器 權(quán)重
!important 1/0(無窮大)
內(nèi)聯(lián)樣式 1000
ID 100
類/偽類/屬性 10
元素/偽元素 1
通配符/子選擇器/相鄰選擇器 0
!important > 行內(nèi)樣式(權(quán)重1000) > ID 選擇器(權(quán)重 100) > 類選擇器(權(quán)重 10) > 標(biāo)簽(權(quán)重1) > 通配符 > 繼承 > 瀏覽器默認(rèn)屬性
2.繼承性
繼承得到的樣式的優(yōu)先級(jí)是最低的,在任何時(shí)候,只要元素本身有同屬性的樣式定義,就可以覆蓋掉繼承值。
在存在多個(gè)繼承樣式時(shí),層級(jí)關(guān)系距離當(dāng)前元素最近的父級(jí)元素的繼承樣式,具有相對(duì)最高的優(yōu)先級(jí)。
有哪些屬性是可以繼承的呢,我們簡單分一下類:
font-family、font-size、font-weight 等 f 開頭的 CSS 樣式。
text-align、text-indent 等 t 開頭的樣式。
color。
詳細(xì)的規(guī)則,請(qǐng)看下圖:
3.層疊性
層疊就是瀏覽器對(duì)多個(gè)樣式來源進(jìn)行疊加,最終確定結(jié)果的過程。
CSS 之所以有「層疊」的概念,是因?yàn)橛卸鄠€(gè)樣式來源。
CSS 層疊性是指 CSS 樣式在針對(duì)同一元素配置同一屬性時(shí),依據(jù)層疊規(guī)則(權(quán)重)來處理沖突,選擇應(yīng)用權(quán)重高的 CSS 選擇器所指定的屬性,一般也被描述為權(quán)重高的覆蓋權(quán)重低的,因此也稱作層疊。
示例代碼三:
<div >
<p class="two one">Jartto's blog</p>
</div>
<style>
.one{color: red;}
.two{color: blue;}
<style>
如果兩個(gè)類選擇器同時(shí)作用呢,究竟以誰為準(zhǔn)?這里我們要考慮樣式表中兩個(gè)類選擇器的先后順序,后面的會(huì)覆蓋前面的,所以文本當(dāng)然顯示藍(lán)色了。
升級(jí)代碼:
<div>
<div>
<div>Jartto's blog</div>
</div>
</div>
<style>
div div div { color: green; }
div div { color: red; }
div { color: yellow; }
<style>
這個(gè)比較直接,算一下權(quán)重,誰大聽誰的。
繼續(xù)升級(jí):
<div id="box1" class="one">
<div id="box2" class="two">
<div id="box3" class="three"> Jartto's blog </div>
</div>
</div>
<style>
.one .two div { color : red; }
div #box3 { color : yellow; }
#box1 div { color : blue; }
</style>
權(quán)重:
0 0 2 1
0 1 0 1
0 1 0 1
驗(yàn)證一下:
<div id="box1" class="one">
<div id="box2" class="two">
<div id="box3" class="three"> Jartto's blog </div>
</div>
</div>
<style>
.one .two div { color : red; }
#box1 div { color : blue; }
div .three { color : green; }
</style>
權(quán)重:
0 0 2 1
0 1 0 1
0 0 1 1
如果你對(duì)上面這些問題都了如指掌,那么恭喜你,基礎(chǔ)部分順利過關(guān),可以繼續(xù)升級(jí)了!
4.CSS 語法解析過程
1.我們來把 CSS 拎出來看一下,HTML Parser 會(huì)生成 DOM 樹,而 CSS Parser 會(huì)將解析結(jié)果附加到 DOM 樹上,如下圖:
2.CSS 有自己的規(guī)則,一般如下:WebKit 使用 Flex 和 Bison 解析器生成器,通過 CSS語法文件自動(dòng)創(chuàng)建解析器。Bison 會(huì)創(chuàng)建自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。
這兩種解析器都會(huì)將 CSS 文件解析成 StyleSheet 對(duì)象,且每個(gè)對(duì)象都包含 CSS 規(guī)則。CSS 規(guī)則對(duì)象則包含選擇器和聲明對(duì)象,以及其他與 CSS 語法對(duì)應(yīng)的對(duì)象。
3.CSS 解析過程會(huì)按照 Rule,Declaration 來操作:
4.那么他是如何解析的呢,我們不妨打印一下 CSS Rules:
控制臺(tái)輸入:
document.styleSheets[0].cssRules
image.png
打印出來的結(jié)果大致分為幾類:
cssText:存儲(chǔ)當(dāng)前節(jié)點(diǎn)規(guī)則字符串
parentRule:父節(jié)點(diǎn)的規(guī)則
parentStyleSheet:包含 cssRules,ownerNode,rules 規(guī)則
…
規(guī)則貌似有點(diǎn)看不懂,不用著急,我們接著往下看。
5.CSS 解析和 Webkit 有什么關(guān)系?
CSS 依賴 WebCore 來解析,而 WebCore 又是 Webkit 非常重要的一個(gè)模塊。
要了解 WebCore 是如何解析的,我們需要查看相關(guān)源碼:
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。
注意:源碼是為了參考理解,不需要逐行閱讀!
Webkit 使用了自動(dòng)代碼生成工具生成了相應(yīng)的代碼,也就是說詞法分析和語法分析這部分代碼是自動(dòng)生成的,而 Webkit 中實(shí)現(xiàn)的 CallBack 函數(shù)就是在 CSSParser 中。
這時(shí)候就不得不提到 AST 了,我們繼續(xù)剖析。
補(bǔ)充閱讀:Webkit 對(duì) CSS 支持
6.關(guān)于 AST
如果對(duì) AST 還不了解,請(qǐng)移步 AST 抽象語法樹。這里我們不做過多解釋,主要圍繞如何解析這一過程展開,先來看一張 Babel 轉(zhuǎn)換過程圖:
ast
我們來舉一個(gè)簡單的例子,聲明一個(gè)箭頭函數(shù),如下:
let jarttoTest = () => {
// Todo
}
通過在線編譯,生成如下結(jié)果:
從上圖我們可以看出:我們的箭頭函數(shù)被解析成了一段標(biāo)準(zhǔn)代碼,包含了類型,起始位置,結(jié)束位置,變量聲明的類型,變量名,函數(shù)名,箭頭函數(shù)表達(dá)式等等。
標(biāo)準(zhǔn)的解析代碼,我們可以對(duì)其進(jìn)行一些加工和處理,之后通過相應(yīng) API 輸出。
很多場景都會(huì)用到這個(gè)過程,如:
- JS 反編譯,語法解析。
- Babel 編譯 ES6 語法。
- 代碼高亮。
- 關(guān)鍵字匹配。
- 作用域判斷。
- 代碼壓縮。
- …
場景千千萬,但是都離不開一個(gè)過程,那就是:
AST 轉(zhuǎn)換過程:解析 - 轉(zhuǎn)換 - 生成
到這里,CSS 如何解析的來龍去脈我們已經(jīng)非常清楚了,可以回到文章開頭的那個(gè)流程圖了,相信你一定會(huì)有另一翻感悟。
CSS 選擇器執(zhí)行順序
渲染引擎解析 CSS 選擇器時(shí)是從右往左解析,這是為什么呢?舉個(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>
<style>
div > div.jartto p span.yellow {
color: yellow;
}
</style>
我們按照「從左到右」的方式進(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)。
我們按照「從右向左」的方式進(jìn)行分析:
1.首先就查找到 class=“yellow” 的 span 元素。
2.接著檢測父節(jié)點(diǎn)是否為 p 元素,如果不是則進(jìn)入同級(jí)其他節(jié)點(diǎn)的遍歷,如果是則繼續(xù)匹配父節(jié)點(diǎn)滿足 class=“jartto” 的 div 容器。
3.這樣就又減少了集合的元素,只有符合當(dāng)前的子規(guī)則才會(huì)匹配再上一條子規(guī)則。
綜上所述,我們可以得出結(jié)論:
瀏覽器 CSS 匹配核心算法的規(guī)則是以從右向左方式匹配節(jié)點(diǎn)的。
這樣做是為了減少無效匹配次數(shù),從而匹配快、性能更優(yōu)。
所以,我們?cè)跁鴮?CSS Selector 時(shí),從右向左的 Selector Term 匹配節(jié)點(diǎn)越少越好。
不同 CSS 解析器對(duì) CSS Rules 解析速度差異也很大,感興趣的童鞋可以看看 CSS 解析引擎,這里不再贅述。
高效的 ComputedStyle
瀏覽器還有一個(gè)非常棒的策略,在特定情況下,瀏覽器會(huì)共享 Computed Style,網(wǎng)頁中能共享的標(biāo)簽非常多,所以能極大的提升執(zhí)行效率!
如果能共享,那就不需要執(zhí)行匹配算法了,執(zhí)行效率自然非常高。
如果兩個(gè)或多個(gè) Element 的 ComputedStyle 不通過計(jì)算可以確認(rèn)他們相等,那么這些 ComputedStyle 相等的 Elements 只會(huì)計(jì)算一次樣式,其余的僅僅共享該 ComputedStyle。
<section class="one">
<p class="desc">One</p>
</section>
<section class="one">
<p class="desc">two</p>
</section>
如何高效共享 Computed Style ?
1.TagName 和 Class 屬性必須一樣。
2.不能有 Style 屬性。哪怕 Style 屬性相等,他們也不共享。
3.不能使用 Sibling selector,譬如: first-child、 :last-selector、 + selector。
4.mappedAttribute 必須相等
為了更好的說明,我們?cè)倥e兩個(gè)例子:
不能共享,上述規(guī)則 2:
<p style="color:red">jartto's</p>
<p style="color:red">blog</p>
可以共享,上述規(guī)則 4:
<p align="middle">jartto's</p>
<p align="middle">blog</p>
到這里,相信你對(duì) ComputedStyle 有了更多的認(rèn)識(shí),代碼也就更加精煉和高效了。
CSS 書寫順序?qū)π阅苡杏绊憜幔?br> 需要注意的是:瀏覽器并不是一獲取到 CSS 樣式就立馬開始解析,而是根據(jù) CSS 樣式的書寫順序?qū)⒅凑?DOM 樹的結(jié)構(gòu)分布渲染樣式,然后開始遍歷每個(gè)樹結(jié)點(diǎn)的 CSS 樣式進(jìn)行解析,此時(shí)的 CSS 樣式的遍歷順序完全是按照之前的書寫順序。
在解析過程中,一旦瀏覽器發(fā)現(xiàn)某個(gè)元素的定位變化影響布局,則需要倒回去重新渲染。
我們來看看下面這個(gè)代碼片段:
width: 150px;
height: 150px;
font-size: 24px;
position: absolute;
當(dāng)瀏覽器解析到 position 的時(shí)候突然發(fā)現(xiàn)該元素是絕對(duì)定位元素需要脫離文檔流,而之前卻是按照普通元素進(jìn)行解析的,所以不得不重新渲染。
渲染引擎首先解除該元素在文檔中所占位置,這就導(dǎo)致了該元素的占位情況發(fā)生了變化,其他元素可能會(huì)受到它回流的影響而重新排位。
我們對(duì)代碼進(jìn)行調(diào)整:
position: absolute;
width: 150px;
height: 150px;
font-size: 24px;
這樣就能讓渲染引擎更高效的工作,可是問題來了:
在實(shí)際開發(fā)過程中,我們?nèi)绾文鼙WC自己的書寫順序是最優(yōu)呢?
這里有一個(gè)規(guī)范,建議順序大致如下:
定位屬性
position display float left top right bottom overflow clear z-index
自身屬性
width height padding border margin background
文本屬性
text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow
CSS3 中新增屬性
content box-shadow border-radius transform
當(dāng)然,我們需要知道這個(gè)規(guī)則就夠了,剩下的可以交給一些插件去做,譬如 CSSLint(能用代碼實(shí)現(xiàn)的,千萬不要去浪費(fèi)人力)。
7.優(yōu)化策略
我們從瀏覽器構(gòu)成,聊到了渲染引擎,再到 CSS 的解析原理,最后到執(zhí)行順序,做了一系列的探索。期望大家能從 CSS 的渲染原理中了解整個(gè)過程,從而寫出更高效的代碼。
1. 使用 id selector 非常的高效
在使用 id selector 的時(shí)候需要注意一點(diǎn):因?yàn)?id 是唯一的,所以不需要既指定 id 又指定 tagName:
/* Bad */
p#id1 {color:red;}
/* Good */
#id1 {color:red;}
- 避免深層次的 node
譬如:
/* Bad */
div > div > div > p {color:red;}
/* Good */
p-class{color:red;}
- 不要使用 attribute selector
如:p[att1=”val1”],這樣的匹配非常慢。更不要這樣寫:p[id="id1"],這樣將 id selector 退化成 attribute selector。
/* Bad */
p[id="jartto"]{color:red;}
p[class="blog"]{color:red;}
/* Good */
#jartto{color:red;}
.blog{color:red;}
- 將瀏覽器前綴置于前面,將標(biāo)準(zhǔn)樣式屬性置于最后
類似:
.foo {
-moz-border-radius: 5px;
border-radius: 5px;
}
可以參考這個(gè) Css 規(guī)范。
- 遵守 CSSLint 規(guī)則
font-faces 不能使用超過5個(gè)web字體
import 禁止使用@import
regex-selectors 禁止使用屬性選擇器中的正則表達(dá)式選擇器
universal-selector 禁止使用通用選擇器*
unqualified-attributes 禁止使用不規(guī)范的屬性選擇器
zero-units 0后面不要加單位
overqualified-elements 使用相鄰選擇器時(shí),不要使用不必要的選擇器
shorthand 簡寫樣式屬性
duplicate-background-images 相同的url在樣式表中不超過一次
減少 CSS 文檔體積
移除空的 CSS 規(guī)則(Remove empty rules)。
值為 0 不需要單位。
使用縮寫。
屬性值為浮動(dòng)小數(shù) 0.xx,可以省略小數(shù)點(diǎn)之前的 0。
不給 h1-h6 元素定義過多的樣式。CSS Will Change
WillChange 屬性,允許作者提前告知瀏覽器的默認(rèn)樣式,使用一個(gè)專用的屬性來通知瀏覽器留意接下來的變化,從而優(yōu)化和分配內(nèi)存。不要使用 @import
使用 @import 引入 CSS 會(huì)影響瀏覽器的并行下載。
使用 @import 引用的 CSS 文件只有在引用它的那個(gè) CSS 文件被下載、解析之后,瀏覽器才會(huì)知道還有另外一個(gè) CSS 需要下載,這時(shí)才去下載,然后下載后開始解析、構(gòu)建 Render Tree 等一系列操作。
多個(gè) @import 會(huì)導(dǎo)致下載順序紊亂。在 IE 中,@import 會(huì)引發(fā)資源文件的下載順序被打亂,即排列在 @import 后面的 JS 文件先于 @import 下載,并且打亂甚至破壞 @import自身的并行下載。
- 避免過分回流/重排(Reflow)
瀏覽器重新計(jì)算布局位置與大小。
常見的重排元素:
width
height
padding
margin
display
border-width
border
top
position
font-size
float
text-align
overflow-y
font-weight
overflow
left
font-family
line-height
vertical-align
right
clear
white-space
bottom
min-height
- 高效利用 computedStyle
公共類。
慎用 ChildSelector。
盡可能共享。 - 減少昂貴屬性
當(dāng)頁面發(fā)生重繪時(shí),它們會(huì)降低瀏覽器的渲染性能。所以在編寫 CSS 時(shí),我們應(yīng)該盡量減少使用昂貴屬性,如:
box-shadow。
border-radius。
filter。
:nth-child。
依賴?yán)^承
如果某些屬性可以繼承,那么自然沒有必要再寫一遍。遵守 CSS 順序規(guī)則
上面就是對(duì)本文的一個(gè)總結(jié),你了解 CSS 具體的實(shí)現(xiàn)原理,曉得規(guī)避錯(cuò)誤書寫方式,知道為什么這么優(yōu)化,這就夠了。
性能優(yōu)化,進(jìn)無止境。