經典面試題:瀏覽器是怎樣解析CSS的?


摘要: 理解瀏覽器原理。

解析

一旦 CSS 被瀏覽器下載,CSS 解析器就會被打開來處理它遇到的任何 CSS。這可以是單個文檔內的 CSS、<style>標記內的 CSS,也可以是 DOM 元素的style屬性內嵌的 CSS。所 有 CSS 都根據語法規范進行解析和標記。解析完成后,就會生成有一個包含所有選擇器、屬性和屬性各自值的數據結構。

例如,考慮以下 CSS:

.fancy-button {
    background: green;
    border: 3px solid red;
    font-size: 1em;
}

以上 CSS 片段將生成如下數據結構,以便在后續的過程中方便使用:

值得注意的一件事是,瀏覽器將 backgroundborder 的簡寫還原成普通寫法,也就是一個一個屬性的聲明,因為簡單寫主要方便開發人員的編寫,但從這里開始,瀏覽器只處理普通寫法。完解析成之后,瀏覽器引擎繼續構建 DOM 樹。

計算

既然我們已經解析了現有內容中的所有樣式,接着就是對它們進行樣式計算了。我們嘗試盡量對所有值減少到一個標准化的計算值。當離開計算階段時,任何維度值都被縮減為三個可能的輸出之一:auto、百分比或像素值。為了清晰起見,讓我們看幾個例子,看 web 開發人員寫了什么,以及計算后的結果:

現在我們已經計算了數據存儲中的所有值,是時候處理級聯了。

級聯

由於 CSS 來源有多種,所以瀏覽器需要一種方法來確定哪些樣式應該應用於給定的元素。為此,瀏覽器使用一個名為 特殊性(specificity) 的公式,它計算選擇器中使用的標記、類、id 和屬性選擇器的數值,以及 !important聲明的數值。

通過內聯 style 屬性在元素上定義的樣式被賦予一個等級,該等級優先於 <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 盒子,每個盒子都有一個 marginborderpaddingcontent

在本節中,我們將討論以下 CSS 布局概念:

  • 格式化上下文(FC):有許多類型的格式化上下文,其中大多數 Web 開發人員通過更改 display 元素的值來調用。一些最常見的格式化上下文是塊(塊格式化上下文或BFC),flex,grid,table-cells 和 inline。其他一些 CSS 也可以強制使用新的格式化上下文,例如 position: absolutefloat 或使用 multi-colum
  • 包含塊:這是用於解析樣式的祖先塊。
  • 內聯方向:這是文本布局的方向,由元素的書寫模式決定。 在拉丁語言中,這是水平軸,在 CJK 語言中,這是垂直軸。
  • 塊方向:此行為與內聯方向完全相同,但與內聯軸垂直。因此,對於基於拉丁語的語言,這是垂直軸,而在 CJK 語言中,這是水平軸。

解析 Auto

請記住,在計算階段,維度值可以是三個值之一:auto、百分數或像素。布局的目的是在Box Tree中調整所有盒子的大小和位置,使它們為繪制做好准備。

下面示例可以更容易地理解Box Tree是如何構建的。為了便於理解,這里不顯示單獨的 CSS 框,只顯示主盒(principal box)。讓我們看看一個基本的 “Hello world” 布局使用以下代碼:

<body>
    <p>Hello world</p>
    <style>
        body {
            width: 50px;
        }
    </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

處理浮動 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: 400px;
        max-width: 800px;
        background: rgb(191, 191, 191);
        padding: 5px;
    }

    button {
        float: left;
        background: rgb(210, 32, 79);
        padding: 3px 10px;
        border: 2px solid black;
        margin: 5px;
    }

    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: 300px;
        }
    </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: 300px;
            height: 300px;
            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: 100px;
        height: 100px;
        animation: pulse 1s 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: 3px 10px;
    border: 2px solid black;
}

button:hover {
    background: teal;
    color: black;
}

我們在這里添加的是一個偽類,它告訴瀏覽器在用戶懸停在按鈕上時更改按鈕的背景和文本顏色。這就引出了一個問題,瀏覽器如何處理這個問題?

瀏覽器不斷跟蹤各種輸入,當這些輸入正在移動時,它會經歷稱為命中測試的過程。 對於此示例,該過程如下所示:

  1. 用戶將鼠標移到按鈕上。
  2. 瀏覽器觸發鼠標已移動的事件,並進入命中測試算法,該算法本質上是問“鼠標正在觸摸哪個 box”
  3. 該算法返回鏈接到我們的 “SHARE IT” 按鈕。
  4. 瀏覽器會問這個問題:“既然有鼠標在你上方盤旋,我應該做什么?”。
  5. 它快速運行此框及其子框的樣式/級聯,並確定:hover 在聲明塊內部有一個僅使用繪制樣式調整的偽類。
  6. 它將這些樣式掛起 DOM 元素(正如我們在級聯階段所學到的),在這種情況下是按鈕。
  7. 它跳過布局,直接繪制一個新的位圖。
  8. 新的位圖被傳遞給合成程序,然后傳遞給用戶。

總結

希望這部分對你關於 css 解析過程多多少少有點幫助,共進步!

關於Fundebug

Fundebug專注於JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等眾多品牌企業。歡迎大家免費試用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM