經典面試題:瀏覽器是怎樣解析CSS的?
摘要:
理解瀏覽器原理。
原文:瀏覽器解析 CSS 樣式的過程
作者:前端小智
解析
一旦 CSS 被瀏覽器下載,CSS 解析器就會被開啟來處理它遇到的任何 CSS。這可以是單個文件內的 CSS、
標記內的 CSS,也可以是 DOM 元素的
style
屬性內嵌的 CSS。所 有 CSS 都根據語法規範進行解析和標記。解析完成後,就會生成有一個包含所有選擇器、屬性和屬性各自值的資料結構。
例如,考慮以下 CSS:
。
fancy-button
{
background
:
green
;
border
:
3
px
solid
red
;
font-size
:
1
em
;
}
以上 CSS 片段將生成如下資料結構,以便在後續的過程中方便使用:
值得注意的一件事是,瀏覽器將
background
和
border
的簡寫還原成普通寫法,也就是一個一個屬性的宣告,因為簡單寫主要方便開發人員的編寫,但從這裡開始,瀏覽器只處理普通寫法。完解析成之後,瀏覽器引擎繼續構建 DOM 樹。
計算
既然我們已經解析了現有內容中的所有樣式,接著就是對它們進行樣式計算了。我們嘗試儘量對所有值減少到一個標準化的計算值。當離開計算階段時,任何維度值都被縮減為三個可能的輸出之一:
auto
、百分比或畫素值。為了清晰起見,讓我們看幾個例子,看 web 開發人員寫了什麼,以及計算後的結果:
現在我們已經計算了資料儲存中的所有值,是時候處理級聯了。
級聯
由於 CSS 來源有多種,所以瀏覽器需要一種方法來確定哪些樣式應該應用於給定的元素。為此,瀏覽器使用一個名為
特殊性(specificity)
的公式,它計算選擇器中使用的標記、類、id 和屬性選擇器的數值,以及
!important
宣告的數值。
透過內聯
style
屬性在元素上定義的樣式被賦予一個等級,該等級優先於
塊或外部樣式表中的任何樣式。如果 Web 開發人員使用
!important
某個值,則該值將勝過任何 CSS,無論其位置如何,除非還有
!important
內聯。
同一級別的個數,數量多的優先順序高,假設同樣即比較下一級別的個數。至於各級別的優先順序例如以下:
!important > 內聯 > ID > 類 > 標籤 | 偽類 | 屬性選擇 > 偽物件 > 萬用字元 > 繼承
選擇器的特殊性由選擇器本身的元件確定,特殊性值表述為 5 個部分,如:
0,0,1,0,1
(1)、對於選擇器中給定的各個
!important
屬性值,加 1,0,0,0,0 。
(2)、對於選擇器中給定的各個 ID 屬性值,加 0,0,1,0,0 。
(3)、對於選擇器中給定的各個類屬性值、屬性選擇器或偽類,加 0,0,0,1,0 。
(4)、對於選擇器中給定的各個元素和偽元素,加 0,0,0,0,1 。偽元素是否具有特殊性?在這方面 CSS2 有些自相矛盾,不過 CSS2。1 很清楚的指出,偽元素具有特殊性,而且特殊性為 0,0,0,0,1,同元素特殊性相同。
(4)、結合符(+ > [] ^= $= 等等特殊符號)和萬用字元(*)對特殊性沒有任何貢獻,此外萬用字元的特殊性為 0,0,0,0,0。全是 0 有什麼意義呢?當然有意義!子元素繼承祖先元素的樣式根本沒有特殊性,因此當出現這種情況後,萬用字元選擇器定義的樣式宣告也要優先於子元素繼承來的樣式宣告。因為就算特殊性是 0,也比沒有特殊性可言要強。
為了說明這一點,讓我們說明一些選擇器及其計算後的權重數值:
而當優先順序與多個 CSS 宣告中任意一個宣告的優先順序相等的時候,CSS 中最後的那個宣告將會被應用到元素上。
在下面的示例中,
div
將具有藍色背景。
div {
background: red;
}
div {
background: blue;
}
現在 CSS 將生成以下資料結構,在本文中,我們將繼續在此基礎上進行構建。
CSS 也有來源,但它們的用途不同:
CSS 資訊可以從各種來源提供,這些來源可以是 使用者(user) 和 作者(author) 及 使用者代理/瀏覽器(user agent),優先順序如下:
使用者樣式
瀏覽器還允許使用者設定網頁的樣式,例如,我們用 IE 瀏覽網站的時候,都可以透過瀏覽器檢視選單下的樣式或者文字大小子選單來設定網頁實際的顯示效果。
作者樣式
網頁建立者建立的樣式表,一般會 css 檔案出現或者是在頁面頭部裡定義的 style,也就是網站原始碼的一部分。例如,大家看百度和谷歌的頁面就不一樣,這就是作者樣式不一樣的結果。
使用者代理/瀏覽器樣式
也就是瀏覽器自身設定用來顯示網站的樣式,不同的瀏覽器可能有不同的樣式表,例如 IE 和 Firefox 的就不一樣,所以大家分別使用這兩種瀏覽器訪問同一個網站的時候,看到實際效果可能就不同。
通常情況下,作者樣式具有最高的重要性,其次是使用者樣式,最後才是瀏覽器樣式,但是如果出現了
!important
標記的話,那麼規則會被改變,透過
!important
可以提高某種樣式的重要性,讓它的優先順序高於其他沒有加該宣告的所有樣式。
讓我們進一步擴充套件我們的資料集,看看當用戶將瀏覽器的字型大小設定為最小
2em
時會發生什麼:
做級聯
當瀏覽器擁有一個完整的資料結構,包含來自所有源的所有宣告時,它將按照規範對它們進行排序。首先,它將按來源排序,然後按特性(specificity)排序,最後按文件順序排序。
從上圖可知,類名為
。fancy-button
優先順序最高(表中越上面優先順序越高)。例如,從上表中,人會注意到使用者的瀏覽器首選項設定優先 於 Web 開發人員的設定樣式。現在,瀏覽器找到與選擇器匹配的所有 DOM 元素,並將得到的計算樣式掛載到匹配的元素,在本例中
div
為類名為
。fancy-button
:
如果您希望瞭解更多關於級聯的工作原理,請檢視官方規範。
CSS 物件模型
雖然到目前為止我們已經做了很多,但還沒有完成。現在我們需要更新 CSS 物件模型(CSSOM)。 CSSOM 位於
document。stylesheets
中,我們需要對其進行更新,以便讓它知道我們目前為止已經解析和計算的所有內容。
Web 開發人員可能在沒有意識到的情況下使用這些資訊。例如,當呼叫 getComputedStyle() 時,如果需要,執行上面指出的相同過程
佈局
現在我們已經應用了一個具有樣式的 DOM 樹,然後開始構建一個用於視覺化目的的樹了。這棵樹出現在所有現代引擎中,被稱為盒子樹(box tree)。為了構造這棵樹,我們遍歷 DOM 樹並建立零個或多個 CSS 盒子,每個盒子都有一個
margin
、
border
、
padding
和
content
。
在本節中,我們將討論以下 CSS 佈局概念:
格式化上下文(FC):有許多型別的格式化上下文,其中大多數 Web 開發人員透過更改
display
元素的值來呼叫。一些最常見的格式化上下文是塊(塊格式化上下文或
BFC
),flex,grid,table-cells 和 inline。其他一些 CSS 也可以強制使用新的格式化上下文,例如
position: absolute
,
float
或使用
multi-colum
。
包含塊:這是用於解析樣式的祖先塊。
內聯方向:這是文字佈局的方向,由元素的書寫模式決定。 在拉丁語言中,這是水平軸,在 CJK 語言中,這是垂直軸。
塊方向:此行為與內聯方向完全相同,但與內聯軸垂直。因此,對於基於拉丁語的語言,這是垂直軸,而在 CJK 語言中,這是水平軸。
解析 Auto
請記住,在計算階段,維度值可以是三個值之一:auto、百分數或畫素。佈局的目的是在
Box Tree
中調整所有盒子的大小和位置,使它們為繪製做好準備。
下面示例可以更容易地理解
Box Tree
是如何構建的。為了便於理解,這裡不顯示單獨的 CSS 框,只顯示主盒(principal box)。讓我們看看一個基本的 “Hello world” 佈局使用以下程式碼:
<
body
>
<
p
>
Hello world
p
>
<
style
>
body
{
width
:
50
px
;
}
style
>
body
>
瀏覽器從 body 元素開始,生成它的主盒(principal box),它的寬度為
50px
,預設高度為
auto
。
現在移動到
p
標籤並生成其主盒(principal box),並且由於
p
標籤預設有邊距(margin),這將影響正文的高度,如下所示:
現在瀏覽器移動到
“Hello world”
文字,這是 DOM 中的文字節點。因此,我們在佈局中生成一個
行內盒(line box)
。請注意,文字溢位了正文,我們將在下一步處理這個問題。
因為加上“world”長度後實際長度比較設定大並且我們沒有設定
overflow
屬性,所以引擎會向其父級報告它在佈局文字時停止的位置。
由於父級已收到其子級無法完成所有內容佈局的指令,因此它會克隆包含所有樣式的
行內盒(line box)
,並傳遞該框的資訊以完成佈局。
佈局完成後,瀏覽器會返回
box tree
,解析尚未解決的所有基於
auto
或基於百分比的值。 在圖中,可以看到正文和段落現在包含所有 “Hello world”,因為它的
height
設定為
auto
。
程式碼部署後可能存在的 BUG 沒法實時知道,事後為了解決這些 BUG,花了大量的時間進行 log 除錯,這邊順便給大家推薦一個好用的 BUG 監控工具 Fundebug。
處理浮動 float
現在讓佈局變得更復雜一點。我們將使用一個普通佈局,其中有一個按鈕,內容為
“Share It”
,並將其浮動到一段文字的左側。浮動本身被認為是
“shrink-to-fit”
上下文。之所以將其稱為“shrink-to-fit”,是因為如果尺寸是自動的,則該框將圍繞其內容進行收縮。
浮動盒子是與這種佈局型別匹配的盒子的一種型別,但是還有許多其他的盒子,例如絕對定位盒子(包括
position: fixed
)和基於自動調整大小的表格單元格,如下程式碼:
<
article
>
<
button
>
SHARE IT
button
>
<
p
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit。 Nullam
pellentesq
p
>
article
>
<
style
>
article
{
min-width
:
400
px
;
max-width
:
800
px
;
background
:
rgb
(
191
,
191
,
191
);
padding
:
5
px
;
}
button
{
float
:
left
;
background
:
rgb
(
210
,
32
,
79
);
padding
:
3
px
10
px
;
border
:
2
px
solid
black
;
margin
:
5
px
;
}
p
{
margin
:
0
;
}
style
>
該過程開始時遵循與“Hello world”示例相同的模式,因此我將跳到我們開始處理浮動按鈕的位置。
由於浮動建立了一個新的塊格式化上下文(BFC),並且是一個
shrink-to-fit
上下文,因此瀏覽器執行一種稱為內容度量的特定佈局型別。
在這種模式下,它看起來與其他佈局相同,但有一個重要的區別,即它是在無限空間中完成的。在此階段,瀏覽器所做的就是以 BFC 的最大和最小寬度佈局 BFC 樹。
在本例中,它使用文字佈局一個按鈕,因此其最窄的大小(包括所有其他 CSS 框)將是最長單詞的大小。在最寬的地方,它將是一行的所有文字,加上 CSS Box。注意:這裡按鈕的顏色不是文字的顏色。這只是為了說明問題。
現在我們知道最小寬度是 86px,最大寬度是 115px,我們將此資訊傳遞迴父類的 box,讓它決定寬度並適當地放置按鈕。在這個場景中,有足夠的空間來適應浮動的最大大小,這就是按鈕的佈局方式。
為了確保瀏覽器遵循標準,並且內容圍繞浮動,瀏覽器更改了
article
的 BFC 的幾何形狀。這個幾何圖形被傳遞給段落,以便在段落佈局期間使用。
從這裡開始,瀏覽器遵循與第一個示例相同的佈局過程——但是它確保任何內聯內容的內聯和塊的起始位置都位於浮動所佔用的約束空間之外。
當瀏覽器繼續沿著樹向下移動並克隆節點時,它將越過約束空間的塊位置。這允許最後一行文字(以及它之前的一行)以內聯方向開始於 content box 的開頭。然後瀏覽器返回到樹中,根據需要解析
auto
和百分數。
瞭解片段(UNDERSTANDING FRAGMENTATION
關於佈局如何工作的最後一個方面是碎片化。 如果你曾經列印過網頁或使用過 CSS 多列,那麼你已經利用了碎片。 碎片化是將內容分開以使其適合不同幾何形狀的邏輯。 讓我們來看看同一個例子,利用 CSS 多列情況:
<
body
>
<
div
>
<
p
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit。 Cras nibh
orci, tincidunt eget enim et, pellentesque condimentum risus。 Aenean
sollicitudin risus velit, quis tempor leo malesuada vel。 Donec
consequat aliquet mauris。 Vestibulum ante ipsum primis in faucibus
p
>
div
>
<
style
>
body
{
columns
:
2
;
column-fill
:
auto
;
height
:
300
px
;
}
style
>
body
>
一旦瀏覽器到達 multicol 格式化上下文盒子,它就會看到它有一組設定的列。
它遵循以前類似的克隆模型,並建立了一個具有正確維度的碎片處理程式,以滿足作者對其列的要求。
然後瀏覽器按照與之前相同的模式儘可能多地佈局行,然後瀏覽器建立另一個碎片管理器,並繼續完成佈局。
繪畫(Painting)
來回顧一下我們現在的情況,我們取出所有的 CSS 內容,對其進行解析,將其級聯到 DOM 樹中,並完成佈局。但是我們還沒有對布圖應用顏色、邊框、陰影和類似的設計處理——處理這些過程被稱為
繪畫
。
繪畫基本上是由 CSS 標準化的,簡單地說,你可以按照以下順序繪畫:
background;
border;
and content。
更多繪畫的順序可檢視 CSS 2。2 Appendix E。
因此,如果我們從前面的“SHARE IT”按鈕開始,並遵循這個過程,它繪製過程大致如下:
完成後,它將轉換為點陣圖,最終每個佈局元素(甚至文字)都成為引擎中的影象。
關於 Z-INDEX
現在,我們大多數的網站都不是由單一的元素組成的。此外,我們經常希望某些元素出現在其他元素之上。為了實現這一點,我們可以利用
z-index
的特性將一個元素疊加到另一個元素上。
這可能感覺就像我們在設計軟體中使用圖層一樣,但是唯一存在的圖層是在瀏覽器的合成器中。看起來好像我們在使用
z-index
建立新層,但實際上並不是這樣,那麼到底是怎麼樣呢?
我們要做的是建立一個新的堆疊上下文。建立一個新的堆疊上下文可以有效地改變你繪製元素的順序。讓我們來看一個例子:
<
body
>
<
div
id
=
“one”
>
Item 1
div
>
<
div
id
=
“two”
>
Item 2
div
>
<
style
>
body
{
background
:
lightgray
;
}
div
{
width
:
300
px
;
height
:
300
px
;
position
:
absolute
;
background
:
white
;
z-index
:
2
;
}
#
two
{
background
:
green
;
z-index
:
1
;
}
style
>
body
>
如果沒有使用
z-index
,上面的文件將按照文件順序繪製,這將把
“Item 2”
置於
“Item 1”
之上。但由於
z-index
的影響,繪畫順序發生了變化。讓我們逐步完成每個階段,類似於我們之前完成佈局的方式。
瀏覽器以根框開頭,我們在後臺畫畫。
然後瀏覽器按照文件順序遍歷較低層次的堆疊上下文(在本例中是“Item 2”),並開始按照上面的規則繪製該元素。
然後它遍歷到下一個最高的堆疊上下文(在本例中是“Item 1”),並按照 CSS 2。2 中定義的順序繪製它。
z-index
不影響顏色,隻影響哪些元素對使用者可見,因此也不影響哪些文字和顏色可見。
組成(COMPOSITION)
在這個階段,我們至少有一個位圖從繪畫傳遞到合成。合成程式的工作是建立一個或多個層,並將點陣圖呈現到螢幕上供終端使用者檢視。
此時一個合理的問題是,“為什麼任何站點都需要不止一個位圖或合成層?”,根據我們目前看到的例子,我們真的不會這麼做。我們來看一個稍微複雜一點的例子。假設在一個假設的世界中,Office 團隊想讓 Clippy 重新上線,他們想透過 CS S 轉換讓 Clippy 跳動來吸引人們對他的注意。
動畫 Clippy 的程式碼可以是這樣的:
<
div
class
=
“clippy”
>
div
>
<
style
>
。
clippy
{
width
:
100
px
;
height
:
100
px
;
animation
:
pulse
1
s
infinite
;
background
:
url
(
clippy。svg
);
}
@
keyframes
pulse
{
from
{
transform
:
scale
(
1
,
1
);
}
to
{
transform
:
scale
(
2
,
2
);
}
}
style
>
當瀏覽器讀取 web 開發人員希望在無限迴圈中為 Clippy 新增動畫時,它有兩個選項:
它可以返回到動畫的每一幀的重繪階段,並生成一個新的點陣圖以返回合成器。
或者它可以生成兩個不同的點陣圖,並允許合成程式僅在應用了該動畫的層上執行動畫本身。
在大多數情況下,瀏覽器將選擇選項 2 並生成以下內容(我有意簡化了 Word Online 為此示例生成的圖層數量):
然後,它將重新組合剪輯點陣圖在正確的位置,並處理脈動動畫。這對於效能來說是一個很好的優勢,因為在許多引擎中,合成程式是在它自己的執行緒上的,這樣就可以解除主執行緒的阻塞。如果瀏覽器選擇上面的選項 1,它將不得不阻塞每一幀以完成相同的結果,這將對終端使用者的效能和響應能力產生負面影響。
創造互動的視覺
正如我們剛剛瞭解到的,我們使用了所有的樣式和 DOM,並生成了一個呈現給終端使用者的影象。那麼瀏覽器如何建立互動性的假象呢?嗯,我相信你現在已經學過了,所以讓我們看一個例子,用我們的 “SHARE IT” 按鈕作為類比:
button
{
float
:
left
;
background
:
rgb
(
210
,
32
,
79
);
padding
:
3
px
10
px
;
border
:
2
px
solid
black
;
}
button
:
hover
{
background
:
teal
;
color
:
black
;
}
我們在這裡新增的是一個偽類,它告訴瀏覽器在使用者懸停在按鈕上時更改按鈕的背景和文字顏色。這就引出了一個問題,瀏覽器如何處理這個問題?
瀏覽器不斷跟蹤各種輸入,當這些輸入正在移動時,它會經歷稱為
命中測試
的過程。 對於此示例,該過程如下所示:
使用者將滑鼠移到按鈕上。
瀏覽器觸發滑鼠已移動的事件,並進入命中測試演算法,該演算法本質上是問“滑鼠正在觸控哪個 box”
該演算法返回連結到我們的 “SHARE IT” 按鈕。
瀏覽器會問這個問題:“既然有滑鼠在你上方盤旋,我應該做什麼?”。
它快速執行此框及其子框的樣式/級聯,並確定
:hover
在宣告塊內部有一個僅使用繪製樣式調整的偽類。
它將這些樣式掛起 DOM 元素(正如我們在級聯階段所學到的),在這種情況下是按鈕。
它跳過佈局,直接繪製一個新的點陣圖。
新的點陣圖被傳遞給合成程式,然後傳遞給使用者。
總結
希望這部分對你關於 css 解析過程多多少少有點幫助,共進步!
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node。js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用!