經(jīng)典面試題:瀏覽器是怎樣解析CSS的?

摘要: 理解瀏覽器原理。

解析

一旦 CSS 被瀏覽器下載,CSS 解析器就會被打開來處理它遇到的任何 CSS。這可以是單個文檔內(nèi)的 CSS、<style>標(biāo)記內(nèi)的 CSS,也可以是 DOM 元素的style屬性內(nèi)嵌的 CSS。所 有 CSS 都根據(jù)語法規(guī)范進(jìn)行解析和標(biāo)記。解析完成后,就會生成有一個包含所有選擇器、屬性和屬性各自值的數(shù)據(jù)結(jié)構(gòu)。

例如,考慮以下 CSS:

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

以上 CSS 片段將生成如下數(shù)據(jù)結(jié)構(gòu),以便在后續(xù)的過程中方便使用:

值得注意的一件事是,瀏覽器將 backgroundborder 的簡寫還原成普通寫法,也就是一個一個屬性的聲明,因?yàn)楹唵螌懼饕奖汩_發(fā)人員的編寫,但從這里開始,瀏覽器只處理普通寫法。完解析成之后,瀏覽器引擎繼續(xù)構(gòu)建 DOM 樹。

計(jì)算

既然我們已經(jīng)解析了現(xiàn)有內(nèi)容中的所有樣式,接著就是對它們進(jìn)行樣式計(jì)算了。我們嘗試盡量對所有值減少到一個標(biāo)準(zhǔn)化的計(jì)算值。當(dāng)離開計(jì)算階段時,任何維度值都被縮減為三個可能的輸出之一:auto、百分比或像素值。為了清晰起見,讓我們看幾個例子,看 web 開發(fā)人員寫了什么,以及計(jì)算后的結(jié)果:

現(xiàn)在我們已經(jīng)計(jì)算了數(shù)據(jù)存儲中的所有值,是時候處理級聯(lián)了。

級聯(lián)

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

通過內(nèi)聯(lián) style 屬性在元素上定義的樣式被賦予一個等級,該等級優(yōu)先于 <style> 塊或外部樣式表中的任何樣式。如果 Web 開發(fā)人員使用 !important 某個值,則該值將勝過任何 CSS,無論其位置如何,除非還有 !important 內(nèi)聯(lián)。

同一級別的個數(shù),數(shù)量多的優(yōu)先級高,假設(shè)同樣即比較下一級別的個數(shù)。至于各級別的優(yōu)先級例如以下:

!important > 內(nèi)聯(lián) > ID > 類 > 標(biāo)簽 | 偽類 | 屬性選擇 > 偽對象 > 通配符 > 繼承

選擇器的特殊性由選擇器本身的組件確定,特殊性值表述為 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)、結(jié)合符(+ > [] ^= $= 等等特殊符號)和通配符(*)對特殊性沒有任何貢獻(xiàn),此外通配符的特殊性為 0,0,0,0,0。全是 0 有什么意義呢?當(dāng)然有意義!子元素繼承祖先元素的樣式根本沒有特殊性,因此當(dāng)出現(xiàn)這種情況后,通配符選擇器定義的樣式聲明也要優(yōu)先于子元素繼承來的樣式聲明。因?yàn)榫退闾厥庑允?0,也比沒有特殊性可言要強(qiáng)。

為了說明這一點(diǎn),讓我們說明一些選擇器及其計(jì)算后的權(quán)重?cái)?shù)值:

而當(dāng)優(yōu)先級與多個 CSS 聲明中任意一個聲明的優(yōu)先級相等的時候,CSS 中最后的那個聲明將會被應(yīng)用到元素上。

在下面的示例中,div 將具有藍(lán)色背景。

div {
  background: red;
}

div {
  background: blue;
}

現(xiàn)在 CSS 將生成以下數(shù)據(jù)結(jié)構(gòu),在本文中,我們將繼續(xù)在此基礎(chǔ)上進(jìn)行構(gòu)建。

來源

CSS 也有來源,但它們的用途不同:

CSS 信息可以從各種來源提供,這些來源可以是 用戶(user) 和 作者(author) 及 用戶代理/瀏覽器(user agent),優(yōu)先級如下:

用戶樣式

瀏覽器還允許用戶設(shè)置網(wǎng)頁的樣式,例如,我們用 IE 瀏覽網(wǎng)站的時候,都可以通過瀏覽器查看菜單下的樣式或者文字大小子菜單來設(shè)置網(wǎng)頁實(shí)際的顯示效果。

作者樣式

網(wǎng)頁創(chuàng)建者建立的樣式表,一般會 css 文件出現(xiàn)或者是在頁面頭部里定義的 style,也就是網(wǎng)站源代碼的一部分。例如,大家看百度和谷歌的頁面就不一樣,這就是作者樣式不一樣的結(jié)果。

用戶代理/瀏覽器樣式

也就是瀏覽器自身設(shè)置用來顯示網(wǎng)站的樣式,不同的瀏覽器可能有不同的樣式表,例如 IE 和 Firefox 的就不一樣,所以大家分別使用這兩種瀏覽器訪問同一個網(wǎng)站的時候,看到實(shí)際效果可能就不同。

通常情況下,作者樣式具有最高的重要性,其次是用戶樣式,最后才是瀏覽器樣式,但是如果出現(xiàn)了 !important 標(biāo)記的話,那么規(guī)則會被改變,通過 !important 可以提高某種樣式的重要性,讓它的優(yōu)先級高于其他沒有加該聲明的所有樣式。

讓我們進(jìn)一步擴(kuò)展我們的數(shù)據(jù)集,看看當(dāng)用戶將瀏覽器的字體大小設(shè)置為最小 2em 時會發(fā)生什么:

做級聯(lián)

當(dāng)瀏覽器擁有一個完整的數(shù)據(jù)結(jié)構(gòu),包含來自所有源的所有聲明時,它將按照規(guī)范對它們進(jìn)行排序。首先,它將按來源排序,然后按特性(specificity)排序,最后按文檔順序排序。

從上圖可知,類名為 .fancy-button優(yōu)先級最高(表中越上面優(yōu)先級越高)。例如,從上表中,人會注意到用戶的瀏覽器首選項(xiàng)設(shè)置優(yōu)先 于 Web 開發(fā)人員的設(shè)置樣式?,F(xiàn)在,瀏覽器找到與選擇器匹配的所有 DOM 元素,并將得到的計(jì)算樣式掛載到匹配的元素,在本例中 div 為類名為 .fancy-button

如果您希望了解更多關(guān)于級聯(lián)的工作原理,請查看官方規(guī)范。

CSS 對象模型

雖然到目前為止我們已經(jīng)做了很多,但還沒有完成。現(xiàn)在我們需要更新 CSS 對象模型(CSSOM)。 CSSOM 位于document.stylesheets 中,我們需要對其進(jìn)行更新,以便讓它知道我們目前為止已經(jīng)解析和計(jì)算的所有內(nèi)容。

Web 開發(fā)人員可能在沒有意識到的情況下使用這些信息。例如,當(dāng)調(diào)用 getComputedStyle() 時,如果需要,運(yùn)行上面指出的相同過程

布局

現(xiàn)在我們已經(jīng)應(yīng)用了一個具有樣式的 DOM 樹,然后開始構(gòu)建一個用于可視化目的的樹了。這棵樹出現(xiàn)在所有現(xiàn)代引擎中,被稱為盒子樹(box tree)。為了構(gòu)造這棵樹,我們遍歷 DOM 樹并創(chuàng)建零個或多個 CSS 盒子,每個盒子都有一個 margin、border、paddingcontent

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

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

解析 Auto

請記住,在計(jì)算階段,維度值可以是三個值之一:auto、百分?jǐn)?shù)或像素。布局的目的是在Box Tree中調(diào)整所有盒子的大小和位置,使它們?yōu)槔L制做好準(zhǔn)備。

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

<body>
    <p>Hello world</p>
    <style>
        body {
            width: 50px;
        }
    </style>
</body>

瀏覽器從 body 元素開始,生成它的主盒(principal box),它的寬度為50px,默認(rèn)高度為auto

現(xiàn)在移動到 p 標(biāo)簽并生成其主盒(principal box),并且由于 p 標(biāo)簽?zāi)J(rèn)有邊距(margin),這將影響正文的高度,如下所示:

現(xiàn)在瀏覽器移動到 “Hello world” 文本,這是 DOM 中的文本節(jié)點(diǎn)。因此,我們在布局中生成一個 行內(nèi)盒(line box) 。請注意,文本溢出了正文,我們將在下一步處理這個問題。

因?yàn)榧由稀皐orld”長度后實(shí)際長度比較設(shè)置大并且我們沒有設(shè)置 overflow 屬性,所以引擎會向其父級報(bào)告它在布局文本時停止的位置。

由于父級已收到其子級無法完成所有內(nèi)容布局的指令,因此它會克隆包含所有樣式的 行內(nèi)盒(line box),并傳遞該框的信息以完成布局。

布局完成后,瀏覽器會返回 box tree,解析尚未解決的所有基于 auto 或基于百分比的值。 在圖中,可以看到正文和段落現(xiàn)在包含所有 “Hello world”,因?yàn)樗?height 設(shè)置為 auto。

代碼部署后可能存在的 BUG 沒法實(shí)時知道,事后為了解決這些 BUG,花了大量的時間進(jìn)行 log 調(diào)試,這邊順便給大家推薦一個好用的 BUG 監(jiān)控工具 Fundebug。

處理浮動 float

現(xiàn)在讓布局變得更復(fù)雜一點(diǎn)。我們將使用一個普通布局,其中有一個按鈕,內(nèi)容為 “Share It”,并將其浮動到一段文本的左側(cè)。浮動本身被認(rèn)為是“shrink-to-fit” 上下文。之所以將其稱為“shrink-to-fit”,是因?yàn)槿绻叽缡亲詣拥?,則該框?qū)@其內(nèi)容進(jìn)行收縮。

浮動盒子是與這種布局類型匹配的盒子的一種類型,但是還有許多其他的盒子,例如絕對定位盒子(包括 position: fixed)和基于自動調(diào)整大小的表格單元格,如下代碼:

<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”示例相同的模式,因此我將跳到我們開始處理浮動按鈕的位置。

由于浮動創(chuàng)建了一個新的塊格式化上下文(BFC),并且是一個 shrink-to-fit 上下文,因此瀏覽器執(zhí)行一種稱為內(nèi)容度量的特定布局類型。

在這種模式下,它看起來與其他布局相同,但有一個重要的區(qū)別,即它是在無限空間中完成的。在此階段,瀏覽器所做的就是以 BFC 的最大和最小寬度布局 BFC 樹。

在本例中,它使用文本布局一個按鈕,因此其最窄的大小(包括所有其他 CSS 框)將是最長單詞的大小。在最寬的地方,它將是一行的所有文本,加上 CSS Box。注意:這里按鈕的顏色不是文字的顏色。這只是為了說明問題。

現(xiàn)在我們知道最小寬度是 86px,最大寬度是 115px,我們將此信息傳遞回父類的 box,讓它決定寬度并適當(dāng)?shù)胤胖冒粹o。在這個場景中,有足夠的空間來適應(yīng)浮動的最大大小,這就是按鈕的布局方式。

為了確保瀏覽器遵循標(biāo)準(zhǔn),并且內(nèi)容圍繞浮動,瀏覽器更改了 article 的 BFC 的幾何形狀。這個幾何圖形被傳遞給段落,以便在段落布局期間使用。

從這里開始,瀏覽器遵循與第一個示例相同的布局過程——但是它確保任何內(nèi)聯(lián)內(nèi)容的內(nèi)聯(lián)和塊的起始位置都位于浮動所占用的約束空間之外。

當(dāng)瀏覽器繼續(xù)沿著樹向下移動并克隆節(jié)點(diǎn)時,它將越過約束空間的塊位置。這允許最后一行文本(以及它之前的一行)以內(nèi)聯(lián)方向開始于 content box 的開頭。然后瀏覽器返回到樹中,根據(jù)需要解析 auto 和百分?jǐn)?shù)。

了解片段(UNDERSTANDING FRAGMENTATION

關(guān)于布局如何工作的最后一個方面是碎片化。 如果你曾經(jīng)打印過網(wǎng)頁或使用過 CSS 多列,那么你已經(jīng)利用了碎片。 碎片化是將內(nèi)容分開以使其適合不同幾何形狀的邏輯。 讓我們來看看同一個例子,利用 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>

一旦瀏覽器到達(dá) multicol 格式化上下文盒子,它就會看到它有一組設(shè)定的列。

它遵循以前類似的克隆模型,并創(chuàng)建了一個具有正確維度的碎片處理程序,以滿足作者對其列的要求。

然后瀏覽器按照與之前相同的模式盡可能多地布局行,然后瀏覽器創(chuàng)建另一個碎片管理器,并繼續(xù)完成布局。

繪畫(Painting)

來回顧一下我們現(xiàn)在的情況,我們?nèi)〕鏊械?CSS 內(nèi)容,對其進(jìn)行解析,將其級聯(lián)到 DOM 樹中,并完成布局。但是我們還沒有對布圖應(yīng)用顏色、邊框、陰影和類似的設(shè)計(jì)處理——處理這些過程被稱為繪畫

繪畫基本上是由 CSS 標(biāo)準(zhǔn)化的,簡單地說,你可以按照以下順序繪畫:

  • background;
  • border;
  • and content.

更多繪畫的順序可查看 CSS 2.2 Appendix E。

因此,如果我們從前面的“SHARE IT”按鈕開始,并遵循這個過程,它繪制過程大致如下:

完成后,它將轉(zhuǎn)換為位圖,最終每個布局元素(甚至文本)都成為引擎中的圖像。

關(guān)于 Z-INDEX

現(xiàn)在,我們大多數(shù)的網(wǎng)站都不是由單一的元素組成的。此外,我們經(jīng)常希望某些元素出現(xiàn)在其他元素之上。為了實(shí)現(xiàn)這一點(diǎn),我們可以利用 z-index 的特性將一個元素疊加到另一個元素上。

這可能感覺就像我們在設(shè)計(jì)軟件中使用圖層一樣,但是唯一存在的圖層是在瀏覽器的合成器中。看起來好像我們在使用 z-index 創(chuàng)建新層,但實(shí)際上并不是這樣,那么到底是怎么樣呢?

我們要做的是創(chuàng)建一個新的堆棧上下文。創(chuàng)建一個新的堆疊上下文可以有效地改變你繪制元素的順序。讓我們來看一個例子:

<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 的影響,繪畫順序發(fā)生了變化。讓我們逐步完成每個階段,類似于我們之前完成布局的方式。

瀏覽器以根框開頭,我們在后臺畫畫。

然后瀏覽器按照文檔順序遍歷較低層次的堆棧上下文(在本例中是“Item 2”),并開始按照上面的規(guī)則繪制該元素。

然后它遍歷到下一個最高的堆棧上下文(在本例中是“Item 1”),并按照 CSS 2.2 中定義的順序繪制它。

z-index 不影響顏色,只影響哪些元素對用戶可見,因此也不影響哪些文本和顏色可見。

組成(COMPOSITION)

在這個階段,我們至少有一個位圖從繪畫傳遞到合成。合成程序的工作是創(chuàng)建一個或多個層,并將位圖呈現(xiàn)到屏幕上供最終用戶查看。

此時一個合理的問題是,“為什么任何站點(diǎn)都需要不止一個位圖或合成層?”,根據(jù)我們目前看到的例子,我們真的不會這么做。我們來看一個稍微復(fù)雜一點(diǎn)的例子。假設(shè)在一個假設(shè)的世界中,Office 團(tuán)隊(duì)想讓 Clippy 重新上線,他們想通過 CS S 轉(zhuǎn)換讓 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>

當(dāng)瀏覽器讀取 web 開發(fā)人員希望在無限循環(huán)中為 Clippy 添加動畫時,它有兩個選項(xiàng):

  • 它可以返回到動畫的每一幀的重繪階段,并生成一個新的位圖以返回合成器。
  • 或者它可以生成兩個不同的位圖,并允許合成程序僅在應(yīng)用了該動畫的層上執(zhí)行動畫本身。

在大多數(shù)情況下,瀏覽器將選擇選項(xiàng) 2 并生成以下內(nèi)容(我有意簡化了 Word Online 為此示例生成的圖層數(shù)量):

然后,它將重新組合剪輯位圖在正確的位置,并處理脈動動畫。這對于性能來說是一個很好的優(yōu)勢,因?yàn)樵谠S多引擎中,合成程序是在它自己的線程上的,這樣就可以解除主線程的阻塞。如果瀏覽器選擇上面的選項(xiàng) 1,它將不得不阻塞每一幀以完成相同的結(jié)果,這將對最終用戶的性能和響應(yīng)能力產(chǎn)生負(fù)面影響。

創(chuàng)造互動的視覺

正如我們剛剛了解到的,我們使用了所有的樣式和 DOM,并生成了一個呈現(xiàn)給最終用戶的圖像。那么瀏覽器如何創(chuàng)建交互性的假象呢?嗯,我相信你現(xiàn)在已經(jīng)學(xué)過了,所以讓我們看一個例子,用我們的 “SHARE IT” 按鈕作為類比:

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

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

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

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

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

總結(jié)

希望這部分對你關(guān)于 css 解析過程多多少少有點(diǎn)幫助,共進(jìn)步!

關(guān)于Fundebug

Fundebug專注于JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java線上應(yīng)用實(shí)時BUG監(jiān)控。 自從2016年雙十一正式上線,F(xiàn)undebug累計(jì)處理了10億+錯誤事件,付費(fèi)客戶有Google、360、金山軟件、百姓網(wǎng)等眾多品牌企業(yè)。歡迎大家免費(fèi)試用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容