圖解 CSS:CSS 層疊和繼承

CSS 中有三個概念是必須要掌握的:層疊繼承權重。今天我們主要來了解 CSS 中的層疊和繼承,對於 CSS 權重這一部分將放到 CSS 的選擇器中來介紹,因爲這一部分和 CSS 的選擇器耦合的更爲緊密。不管是初學者還是有一定工作經驗的同學,花點時間閱讀這篇文章都是很有必要的,這樣有利於你對 CSS 更清楚的瞭解和理解。

在很多 Web 開發人員眼中,CSS 不是一門程序語言,但它真真切切的是一門計算機語言。主要用來爲結構化文檔,比如 HTML、XML 等添加樣式,其主要由 W3C 定義和維護。而 CSS 是由 Cascading Style Sheets 三個詞的首字母縮寫,很多人將其稱爲層疊樣式表或者級聯樣式表。接下來要聊的第一個概念就是 CSS 中的層疊,也對應的是 CSS 中的第一個字母 C 。看到這裏,或許你就知道爲什麼會說層疊是 CSS 的重要概念之一了。

層疊

層疊是 CSS 固有的一個東東,它賦予了層疊樣式表層疊性,也常稱級聯。層疊是一個很強大的工具,如果錯誤的使用它可能會導致樣式表的脆弱性,會致使 Web 開發人員在任何時候不得不進行更改時都感到頭痛。比如說:初學 CSS 的同學,特別是 Web 其他端的程序員(比如服務端、客戶端)在獨立編寫 CSS 的時候,經常會碰到這樣的一個現象:“爲什麼寫的樣式不起作用呢? ” 面對這樣的場景很多人會採用非常暴力的手段來處理,比如通過添加 !important 或直接在 HTML 的元素上添加內聯 CSS。這就是令很多同學感到疑惑的一點?爲什麼要這樣做呢?其實這和接下來要介紹的層疊(也有很多人稱之爲級聯)有很大的關係。

層疊定義

因爲我們將要深入的學習和討論 CSS 層疊是如何工作的相關細節,因此我們有必要的瞭解 W3C 規範是如何定義它的:

The cascade takes a unordered list of declared values for a given property on a given element, sorts them by their declaration’s precedence, and outputs a single cascaded value.

大致意思是:該層疊將獲取給定元素上給定屬性的聲明值的無序列表,按聲明的優先級對它們進行排序,並輸出單個層疊值

CSS 層疊是一種算法,瀏覽器通過它來決定將哪些 CSS 樣式規則應用到一個元素上。很多人喜歡把它看作是 “獲勝” 的樣式,按照 CSS 中的術語來說,它的權重更高(後面我們會深入介紹)。

爲了更好的理解 CSS 層疊,將 CSS 聲明看作具體的 “屬性”(Property)。這些屬性可以是聲明的各個部分,比如說 CSS 選擇器或 CSS 屬性,甚至是 CSS 聲明的位置相關(比如它的原始或源代碼中的位置)。

CSS 層疊將會根據自己的算法,採用這些屬性中的一些,併爲每個屬性分配一個權重。如果 CSS 規則在高優先級上獲勝(選擇器權重高),那麼這個樣式規則就會獲勝,即生效。但是,如果在給定的權重下有兩個規則衝突,算法將繼續” 向下層疊”,並且會檢查低優先級屬性,直到找到一個勝出的規則。

簡單的說,當多個相互衝突的 CSS 聲明應用於同一個元素時,CSS 層疊算法會根據一定的機制,從最高權重到最低權重的順序列出:

接下來圍繞這幾點來進行展開。

來源和重要性

層疊檢查的最高加權屬性是給定規則的重要性和來源的組合。就 CSS 規則的來源而言,規則主要來自三個地方:

注意,用戶可能會修改系統設置(例如,系統配色),這會影響默認樣式表。然而,有些用戶代理實現讓默認樣式表中的值不可改變

這三種樣式表將在一定範圍內重疊,並且它們按照層疊互相影響。

CSS 層疊給每個樣式規則賦予了權重,應用這幾條規則時,權重最大的優先。默認情況下,編寫者樣式表中的規則比用戶樣式表中的規則權重高。CSS 聲明的重要性由適當命名的 !important 語法決定。!important 的 CSS 規則自動將它跳到層疊算法的前面,這也是爲什麼不鼓勵在樣式中使用 !important 的原因之一。覆蓋使用 !important 的樣式只能使用其他的 !important 的規則來完成,如果你的項目足夠大,用的 !important 又足夠地多,那麼你的 CSS 就會變得更爲脆弱,更難以維護。對於 !important 的使用,建議你只在其他所有方法都失效的情況之下使用。

那麼 CSS 中層疊算法又是如何判斷哪個聲明獲勝:

CSS 層疊在判斷哪個聲明獲勝時考慮這兩個屬性的組合。每個組合都有一個權重(類似聲明的部分權重),權重最高的規則獲勝。以下是瀏覽器考慮的各種來源和重要性的組合,按從最高權重到最低權重的順序列出:

當瀏覽器遇到兩個或更多衝突的 CSS 聲明,其中一個在來源和重要性級別獲勝時,CSS 層疊就會解決這個規則。但是,如果相互衝突的聲明具有相同的重要性和來源級別,則層疊將繼續考慮選擇器的權重。

選擇器權重

CSS 層疊中的第二個權重是選擇器的權重。在這個層中,瀏覽器查看 CSS 聲明中使用的選擇器。作爲前端開發人員,我們只能控制編寫者樣式規則。因爲我們無法對來源的規則做太多的更改。但是,你會發現,只要在代碼中不使用!important,對於 CSS 層疊中的選擇器權重這一層,還是有較多的方式可以控制。

對於一個選擇器的權重,將會按下面這樣的規則進行計算:

4個數連起來a-b-c-d(在一個基數很大的數字系統中)表示特殊性,比如下面這樣的示例:

{}                   /* a=0 b=0 c=0 d=0 -> 選擇器權重 = 0,0,0,0 * /
li {}                /* a=0 b=0 c=0 d=1 -> 選擇器權重 = 0,0,0,1 * /
li:first-line {}     /* a=0 b=0 c=0 d=2 -> 選擇器權重 = 0,0,0,2 * /
ul li {}             /* a=0 b=0 c=0 d=2 -> 選擇器權重 = 0,0,0,2 * /
ul ol+li {}          /* a=0 b=0 c=0 d=3 -> 選擇器權重 = 0,0,0,3 */
h1 + [rel=up] {} /* a=0 b=0 c=1 d=1 -> 選擇器權重 = 0,0,1,1 * /
ul ol li.red {}     /* a=0 b=0 c=1 d=3 -> 選擇器權重 = 0,0,1,3 * /
li.red.level {}     /* a=0 b=0 c=2 d=1 -> 選擇器權重 = 0,0,2,1 * /
#x34y {}            /* a=0 b=1 c=0 d=0 -> 選擇器權重 = 0,1,0,0 * /
style=""            /* a=1 b=0 c=0 d=0 -> 選擇器權重 = 1,0,0,0 */

在互聯網上也有很多很形象的圖來解釋 CSS 選擇器權重的,比如下圖:

上圖來自於 @Estelle Weyl 的《 CSS Specificity 》一文。

出現的順序

CSS 層疊算法的最後一個主要層是按源碼中出現的順序來計算。當兩個選擇器具有相同的權重時,在源代碼中最後的聲明獲勝。

因爲 CSS 在層疊中考慮源順序,所以加載樣式表的順序就顯得尤爲重要。如果在 HTML 文檔的<head>引入了兩個樣式表,那麼第二個樣式表將覆蓋第一個樣式表中的規則。

初始和繼承屬性

雖然初始值initial和繼承值inherit並不是 CSS 層疊中真正組成部分,但是如果沒有針對元素的 CSS 聲明,它們將確定發生什麼。通過這種方式,它們確定元素的默認樣式值。

有關於初始和繼承更詳細的介紹,將在下一節中進行詳細地闡述。感興趣的同學,請繼續往下閱讀。

參與層疊計算的 CSS 實體

只有 CSS 聲明,就是屬性名值對,會參與層疊計算。這表示包含 CSS 聲明以外實體的@規則不參與層疊計算,比如包含描述符的@font-face。對於這些情形,@規則是做爲一個整體參與層疊計算,比如@font-face的層疊是由其描述符font-family確定的。如果對同一個描述符定義了多次 @font-face,則最適合的 @font-face是做爲一個整體而被考慮的。

包含在大多數@規則的 CSS 聲明是參與層疊計算的,比如包含於@media@documents或者@supports的 CSS 聲明,但是包含於@keyframes的聲明不參與計算,正如@font-face,它是作爲一個整體參與層疊算法的篩選。

注意 @import @charset 遵循特定的算法,並且不受層疊算法影響

CSS 的 @layer 規則

爲了解決諸如此類的問題(級聯與權重),現代 CSS 新增了一個級聯層的規則,即 @layer 。

簡單地說: 級聯層提供了一種結構化的方式來組織和平衡單一來源中的 CSS 規則,最終決定誰獲勝!。

由於 CSS 的級聯層在 CSS 級聯中有着獨特的地位,使用它有一些好處,使開發者對級聯有更多的控制。CSS 的級聯層一般位於 “Style 屬性”(Style Attribute)和 CSS 選擇器權重(Specificity)之間,即:

我使用下圖來闡述有級聯層 @layer 前後,CSS 級聯的差異:

帶來的直接變化是權重計算規則變了:

有了 CSS 級聯層 @layer 特性之後,你可以拋棄以前的一些 CSS 方法論(例如 ITCSS),因爲 @layer 能更好的幫助你管理 CSS 的級聯。

簡單地說,級聯層 @layer 是 CSS 的一個新特性,它影響着樣式規則的應用和優先級。以下是級聯層的一些關鍵細節:

級聯層爲開發者提供了更精細的樣式管理和組織的能力,使得在大型項目中更容易維護和擴展樣式。

有關於 CSS 級聯層 @layer 特性更詳細的介紹,請移步閱讀《現代 CSS》中的《CSS 分層:@layer 》課程!

繼承

在 CSS 中,每個 CSS 屬性定義的概述都指出了這個屬性是默認繼承的還是默認不繼承的。這決定了當你沒有爲元素的屬性指定值時該如何計算值。當元素的一個 繼承屬性沒有指定值時,則取父元素的同屬性的計算值。只有文檔根元素取該屬性的概述中給定的初始值;當元素的一個非繼承屬性沒有指定值時,則取屬性的初始值。比如:

html {
    font-size: small;
}

這個規則在 HTML 文檔的根元素 html 設置了一個 font-size 屬性,而這個屬性是會被繼承的(在 CSS 中有些屬性天性就會被繼承)。正如上面所示,html 元素的所有後代元素都將被繼承這個屬性,比如下圖中藍框中顯示的一樣:

上圖藍框中告訴 body 元素繼承 html 元素的 font-size: small;。開發者工具中會提示我們 “Inherited from html”。那麼問題來了,在 CSS 中哪些屬性是會被繼承的?其實在 W3C 規範中各個屬性的描述已經很清楚的告訴我們了。比如說 border 屬性,在描述其語法時,在列表中有一個 Inherited 描述項的值爲 no。這也就告訴我們 border 屬性是不能被繼承的。反之,再看 font-size 屬性,語法描述的列表中同樣有一個 Inherited 描述項,只不過它的值不是 no,而是 yes,也就是說 font-size 屬性是會被繼承的。

這決定了當你沒有爲元素的屬性指定值時該如何計算值。

如果你平時閱讀規範仔細的話,不難發現,在介紹每個屬性的語法參數的時候,都會有一個 Initial 參數,該參數主要指定每個屬性的初始值。CSS 屬性已經給出的初始值針對不同的繼承和非繼承屬性有不同的含義:

這樣我們引出兩個概念:初始值繼承值,除了這兩個概念之外,在 CSS 屬性中還有一個計算值,該值由指定的值計算而來:

計算屬性的計算值通常包括將相對值轉換成絕對值,比如 em% 這樣的單位。例如:

font-size: 16px;
padding-top: 2em;

其中 padding-top: 2em 就是一個計算值,其計算出來的值將根據 font-size 做爲基數計算(在此示例中),在此計算出來的值是 32px。然而,有些屬性的百分比值會轉換成百分比的計算值(比如 width )。另外, line-height 屬性值是沒有單位的數字,其值也是一個計算值。

對於 CSS 的計算值,在不同的瀏覽器中其計算出來的值有時候會略有偏差。

如果你感興趣的話,可以打開瀏覽器的開發者工具,查看對應的計算值(比如,Chrome 開發者工具,有一個 Computed 選項,該選項展示的就是對應的 CSS 計算值),如下圖所示:

其中計算值的最主要用處是繼承,包括inherit關鍵詞。

最後總結兩點:

看到這裏,或許你知道了什麼叫繼承和非繼承,以及他們取值方式。但你可能還在糾結,在 CSS 中到底哪些屬性是繼承屬性,哪些不是繼承屬性?其實這個問題我也沒辦法準確的回答您,因爲我也沒有做過這方面的統計。不過我可以告訴大家兩個小經驗:

如果你想準確的知道答案,可以通過這裏整理的屬性表格進行查看。只要 Inherited 選項是 Yes 的都表示是繼承屬性,否則都是非繼承屬性。

處理 CSS 繼承的機制

在 CSS 中提供了處理 CSS 繼承的機制,簡單地講就是 CSS 提供了幾個屬性值,可以用來處理屬性的繼承。這幾個屬性值就是initialinheritunsetrevert。其實除了這四個屬性值之外,還有一個 all 屬性值。雖然這幾個屬性值主要用來幫助大家處理 CSS 屬性繼承的,但他們之間的使用,還是有一定的差異化。先用一張圖來闡述它們之間的差異:

接下來我們一看看這幾個屬性值的實際使用以及對應的差異化。

initial

在 CSS 中,每個屬性都具有一個初始值,其實也就是 CSS 屬性的默認值。在 CSS 規範中,都對每個屬性的初始值做出了相關的定義。比如text-align 的初始值是 leftdisplay 的初始值是 inline

而這裏,我們要說的是 CSS 的關鍵詞 initial

If the cascaded value is the initial keyword, the property’s initial value becomes its specified value.

大致的意思是:“如果層疊值是 initial 關鍵詞,則屬性的初始值將成爲其指定值”。換句話說,如果你在元素樣式的設置中顯式的設置某個屬性的值爲initial 時,其實就表示設置了該屬性的默認值。

從文字上理解可能有點困惑,我們通過一個小例來幫助大家理解。假設我們有一個 <p> 元素,接觸過 CSS 的同學都知道,它是一個塊元素,爲了好看,咱們添加一點修飾的樣式代碼:

p {
    background: #f36;
    padding: 2rem;
    font-size: 2rem;
    color: #fff;
}

效果看起來像下面這樣:

如果我們希望 p 元素變成行內元素時,按照我們以前的處理方式,需要手動處理瀏覽器默認樣式(User-Agent 用戶代理樣式),也就是顯示的重置:

p{
    dispaly: inline;
}

blockinline效果對比如下:

前面提到過 inlinedisplay 的初始值(也就是默認值),而在規範中也提到過:

你在元素樣式的設置中顯示的設置某個屬性的值爲 initial 時,其實就表示設置了該屬性的默認值

也就是說,我們可以給 display 設置initial 關鍵詞:

p {
    display: initial;
}

這個時候得到的效果其實和使用 display:inline 是一樣的:

如果我們通過瀏覽器檢查器中的計算值(Computed)一項可以看出來,display 設置爲 initial 時,會覆蓋用戶代理的樣式值 block

接下來,我們再來看一個繼承屬性 color。所以 <p> 元素的後代元素 <strong> 也會繼承 <p> 元素中設置的 color: #fff 值。如果我們顯式的在 strong 中設置 color 的值爲 initial 時,那麼 strongcolor 將重置爲默認值。由於我們沒有設置默認的 color 顏色,那麼這個時候,瀏覽器將會把一個計算值賦予成 color 的初始值:

inherit

CSS 添加了一個 inherit 關鍵詞屬性值,可以讓元素強制繼承父元素的某個屬性的值。前面也說過,CSS 中有些屬性自動就是可繼承屬性,比如 font-sizecolor 之類,但也有很多屬性又是非繼承屬性,比如 borderborder-radius 之類的。在這裏,如果在非常繼承的屬性上顯示的設置了 inherit 關鍵詞,表示該元素將繼承父元素指定的屬性值或者計算值。

爲了同樣的能更好理解 inherit ,來看一個示例,在這個示例中,我們用 border 來做例子:

<div>
    <div>...</div>
</div>

從效果上可以看出來,div.wrapper 上的 border 樣式並沒有繼承其子元素 div 上。如果我們想讓 .wrapper 的子元素 div 也要具有與 .wrapper 元素的 border 樣式,就需要在該元素 div 上顯式設置相同的 border 樣式:

<div>
    <div>...</div>
</div>

有了 inherit 關鍵詞之後,事情要變得簡單地多,只需要在 .wrapper 的子元素上設置 border: inherit;

<div>
    <div>...</div>
</div>

得到的效果將是一樣的:

上面的示例是父元素設置了 border 樣式,所以其繼承了父元素的 border 樣式。那如果將上面的示例稍做修改,在元素外套一個 div ,而這個 div 不做任何樣式的設置。將又會變成一個什麼樣呢?直接上示例吧:

<div>
    <div>
        <div>...</div>
    </div>
</div>

效果如下:

可以看到 div.ele 僅繼承了其父元素 divborder 屬性的計算值,並未繼承其祖先元素 .wrapperborder 屬性的設置值,通過瀏覽器開發者工具,可以看得一目瞭然:

這個示例說明:僅管元素自身顯式的設置了 inherit 關鍵詞,但是,如果其父元素沒有明確指定樣式,那麼其最終效果將和 revert 的效果一致。即繼承的是其父元素的計算值,也就是瀏覽器默認樣式(User Agent Stylesheet)

revert

revert 值早前被稱之爲 default。表示沒有使用任何屬性值。

我們都知道,如果沒有使用作者樣式表(也就是 Web 開發人員自己寫的樣式表),那麼瀏覽器將會按這樣的過程去檢測,元素調用的樣式:

還是拿示例來說吧。比如我們一個 div 元素,我們並沒有顯式的在自己的樣式表中設置其 display 屬性的值。對於最後的渲染結果,瀏覽器將會使用用戶代理規則的樣式 display:block

根據前面介紹的,就算是我們在 div 中顯式的設置 display:revert,該元素也將使用用戶代理規則中的 display:block 樣式。同樣,我們在另一個 div 元素中使用 display:initial ,根據前面介紹的,那在這個 div 將會採用 display 的初始值 inline 。比如下面的效果:

我們再來看一個繼承屬性的運用場景。因爲在 div 元素上設置了 color:#fff; 樣式,這是用戶寫的樣式,而且 color 是一個繼承屬性,只要是 div 的後代元素都將會繼承 color 的屬性值。根據前面所說 revert 的檢測機制是,先檢測用戶自己寫的樣式,然後再檢測用戶代理樣式,如果兩都沒有,纔會設置 unset 的樣式。所以最終我們看到的效果如下:(第二個設置了 color:initial;)。

unset

unsetinitialinherit 的組合。當屬性設置爲 unset 時,如果它是一個繼承屬性,那麼它相當於是 inherit;如果它不是,則相當於 initial

有一些屬性,如果沒有明確指定,將默認是 inherit。比如,我們給元素設置一個 color,那它將適用於所有默認的子元素。而其它屬性,如 border 則默認是非繼承屬性。

<div>
    <div>...</div>
</div>

此時效果如下:

示例中 color 屬性被繼承了,但 border 屬性沒有被繼承。將上面的示例代碼稍作調整:

<div>
    <div>...</div>
</div>

div.ele 元素的 bordercolor 都設置了 unset 值。也就是說它將運用 initial 或者 inherit 的值。具體取決於哪個值,這得根據屬性的默認行爲是什麼來決定。如果默認屬性是 inherit,那將運用的是 inherit;如果默認屬性是 initial,那將運用的是 initial

上面的示例中,border 屬性採用的是 initialcolor 屬性採用的是 inherit

all

在 CSS 中,all 是一個簡寫屬性,其重設除了 unicode-bididirection 之外的所有屬性至它們的初始值或繼承值。all 有三個值:

來看示例。比如我們一個這樣的一個 HTML 結構:

<div>...<strong>...</strong> ...</div>

給他們設置一些樣式:

body {
    padding: 2vw;
}
div {
    background: #f36;
    padding: 2rem;
    font-size: 2rem;
    color: #fff;
    margin-bottom: 3rem;
}
strong {
    font-size: 3rem;
}

看到的效果如下:

這效果正是我們想要的。div 顯式的指定了backgroundpaddingfont-sizecolormargin-bottom屬性值,而其中backgroundpaddingmargin-bottom是非繼承屬性, 而colorfont-size是繼承屬性。除此之外,div還有一個客戶端代理樣式display:block,這個屬性也是一個非繼承屬性。另外strong元素設置了一繼承屬性font-size,這個元素默認情況下繼承了共父元素的color屬性,同時還繼承了html元素的font-familyline-height屬性。當然,strong元素也有一個客戶端代理樣式font-weight:bold

上面看到的效果是我們平時使用的時候效果。如果這個時候,我們在divstrong同時設置all:inherit;時,得到的效果和前面的效果完全不一樣:

這個時候divstrong重置了當初自己設置的屬性,並且繼承了各自父元素的一些屬性:

所以最後你看到的效果就像上圖那樣子。我們再把all的值設置爲initial

這個時候divstrong樣式都重置到了對應的初始樣式,也就是對應的屬性的默認樣式,包括代理客戶端樣式也重置爲對應屬性的初始值。

最後來看all的值設置爲unset的效果,下面的示例,我只在strong元素上設置all:unset,其效果就足以說明一切:

效果中的第一個是沒設置all:unset,第二個設置了all:unset。這個時候strong元素的font-sizefont-weight繼承了其父元素的font-sizefont-weight

all在 CSS 中有時候是一個屬性,比如這裏說的就是屬性,但有的時候它還是 CSS 中某些屬性的值。比如我們常在transition中用到的all,那這個時候就是屬性值。到目前爲止,CSS 中的all屬性也得到了衆多瀏覽器的支持。

如果你想更深入的瞭解 initialinheritrevertunset ,那麼請移步閱讀《現代 CSS》的《CSS 顯式默認值:inherit 、initial 、revert 和 unset》

CSS 樣式的計算

CSS 屬性的最終值會經過四步計算:

那麼什麼是指定值計算值應用值實際值呢?

指定值

用戶代理必須先根據下列機制(按優先順序)給每個屬性賦值一個指定值:

計算值

指定值通過層疊被處理爲計算值,例如,URI被轉換成絕對的,emex單位被計算爲像素或者絕對長度。計算一個值並不需要用戶代理渲染文檔。用戶代理規則無法處理爲絕對 URI 的話,該URI的計算值就是指定值。

一個屬性的計算值由屬性定義中 Computed Value 行決定。當指定值爲inherit時,計算值的定義可以依據繼承中介紹的規則來計算。即使屬性不適用(於當前元素),其計算值也存在,定義在'Applies To'行。然而,有些屬性可能根據屬性是否適用於該元素來定義元素屬性的計算值。

應用值

處理計算值時,儘可能不要格式化文檔。然而,有些值只能在文檔佈局完成時確定。例如,如果一個元素的寬度是其包含塊的特定百分比,在包含塊的寬度確定之前無法確定這個寬度。應用值是把計算值剩餘的依賴(值)都處理成絕對值後的(計算)結果。

實際值

原則上,應用值應該用於渲染,但用戶代理可能無法在給定的環境中利用該值。例如,用戶代理或許只能用整數像素寬度渲染邊框,因此不得不對寬度的計算值做近似處理,或者用戶代理可能被迫只能用黑白色調而不是全綵色。實際值是經過近似處理後的應用值。

小結

本文涉及了大量的內容,希望它能幫助大家理解樣式是如何受到我們編寫和應用的影響。特別是 CSS 的層疊和繼承。在實際使用的時候,如果很好的運用這些概念和手段,可以更好的幫助大家少寫很多樣式代碼,而且更易於維護自己的 CSS 代碼。

用張圖來表示如下:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qK6s7zvZ5dOQKtbJIPBqXQ