天天看點

【譯】CSS 才不是什麼黑魔法呢CSS 才不是什麼黑魔法呢

<b>本文講的是【譯】CSS 才不是什麼黑魔法呢,</b>

<b></b>

如果你是一名 web 開發者,你可能會時不時地寫一些 CSS。

當你第一次接觸 CSS 時,似乎覺得 CSS 輕而易舉。加邊框,改顔色,小菜一碟。JavaScript 才是前端開發的難點,不是嗎?

但是在你 web 開發生涯中的某天,這個想法變了!更糟糕的是,許多前端社群的開發者早已把 CSS 輕視為一門玩具語言。

然而,事實卻是當我們碰壁時,我們中的許多人實際上未曾深入了解我們編寫的 CSS 做了什麼。

然而直到去年,當我決定專注于前端時,才意識到根本無法像調試 JavaScript 那樣輕松地調試 CSS!

我們都喜歡拿 CSS 開玩笑,但是我們中有多少人真的花時間去嘗試了解我們正在編寫或正在閱讀的 CSS。當我們碰壁時,我們有多少人在解決問題的同時,會深入最底層(看看發生了什麼)? 相反,我們止步于照搬 StackOverflow 上票數最高的答案,或者用一些黑科技(hack)手段随便應付一下,或者我們幹脆撒手不管了:那是一個 feature 而不是一個 bug。

當浏覽器以非預期的方式呈現 CSS 時,開發者常常感到非常困惑。但是 CSS 并不是黑魔法,而作為開發者,我們都明白計算機隻會按照我們的指令去執行。

學習浏覽器的内部工作原理将有助于掌握進階調試技巧和性能優化方案。雖然許多會議的演講會讨論如何修複常見的 bug,但我的演講(和這篇文章)的重點在于為什麼會有這些 bug,為此我将深入介紹浏覽器内部原理,看看我們的 CSS 是如何被解析和呈現。

這一過程大緻可以分為以下幾個步驟:

轉換:從磁盤或網絡讀取 HTML 和 CSS 的原始位元組。

标記化: 将輸入内容分解成一個個有效标記(例如:起始标簽、結束标簽、屬性名、屬性值),分離無關字元(如空格和換行符)。

詞法分析:和 tokenizer(标記生成器)類似,但它還标記每個 token 的類型(類型包括:數字、字元串字面量、相等運算符等等)。

解析: 解析器接收詞法分析器傳遞的 tokens,并嘗試将其與某條文法規則進行比對,比對成功後将之添加到抽象文法樹中。

一旦 DOM 樹和 CSSOM 樹建立完畢,渲染引擎就會将資料結構附加到所謂的渲染樹中,并作為布局過程的一部分。

渲染樹是文檔的可視化表現形式,它按照正确的順序繪制頁面的内容。渲染樹的構造過程遵循以下順序:

從 DOM 樹的根節點開始,周遊每個可見節點

忽略不可見的節點

對于每個可見節點,找到合适的與 CSSOM 比對的規則并應用它們

發送包含内容和計算樣式的可見節點

最後,在螢幕上輸出包含所有可見元素的内容和樣式資訊的渲染樹。

CSSOM 可以對渲染樹産生很大的影響,但不會影響到 DOM 樹。

經曆了布局和渲染樹建構後,浏覽器終于要開始将網頁繪制到螢幕上并合成圖層。

布局:包括計算一個元素占用的空間以及它在螢幕上的位置。父元素可以影響子元素布局,某些情況下子元素也會反過來影響父元素。

繪制:将渲染樹中的每個節點轉換為螢幕上的實際像素的過程。它涉及繪制文本、顔色、圖像、邊框和陰影。繪圖通常在多個圖層上完成,另外由于加載、執行 JavaScript 而改變了 DOM 會導緻多次繪制 。

合成:将所有圖層合并在一個圖層,作為最終螢幕上可見圖層的過程。由于頁面的各個部分可以繪制成多層,是以需要以正确的順序繪制到螢幕上。

繪制時間取決于渲染樹結構,元素的 <code>width</code> 和 <code>height</code> 的值越大,繪制時間就越長。

當人們在談論浏覽器的硬體加速時,絕大多數都是指加速“合成”過程,也就是意味着使用 GPU 來合成網頁的内容。

舉個例子:在使用 CSS <code>transform</code> 屬性時,<code>will-change</code> 屬性能提前告知浏覽器 DOM 元素接下來會有哪些變化。這可以将一些繪制和合成操作移交給 GPU,進而大大提高有大量動畫的頁面的性能。使用 <code>will-change</code> 屬性,對于滾動位置變化、内容變化、不透明度變化以及絕對定位坐标位置變化也有類似的性能收益。

有必要了解一件事:某些 CSS 屬性将導緻重新布局,而其他屬性隻會導緻重新繪制。當然出于性能考慮,最好隻觸發重繪。

舉個例子:元素的顔色改變後,隻會對該元素進行重繪。而元素的位置改變後,會對該元素及其子元素(可能還有同級元素)進行布局和重繪。添加 DOM 節點後,會對該節點進行布局和重繪。一些重大變化(例如增大 <code>html</code> 元素的字型)會導緻整個渲染樹進行重新布局和繪制。

如果你像我一樣,比起 CSSOM 更熟悉 DOM,那麼讓我們來深入了解一下 CSSOM。請務必注意,預設情況下,CSS 會被視為阻塞渲染資源。這意味着浏覽器在建構完 CSSOM 之前,将挂起任何其它程序的渲染。

CSSOM 和 DOM 并不是一一對應的。具有 <code>dispay:none</code> 屬性的元素、<code>&lt;script&gt;</code> 标簽、<code>&lt;meta&gt;</code> 标簽、<code>&lt;head&gt;</code> 元素等等不可見的 DOM 元素不會顯示在渲染樹中。

CSSOM 和 DOM 的另一個差別則在于解析 CSS 使用的是一種上下文無關文法。也就是說,CSS 渲染引擎不會自動補全 CSS 中缺少的文法,然而解析 HTML 建立 DOM 時則剛好相反。

解析 HTML 時,浏覽器不得不結合 HTML 标簽所在的上下文,而且隻遵從 HTML 規範是不夠的,因為 HTML 标簽可能包含一些預設的資訊,并且無論解析成什麼,最終都要渲染出來。(譯注:這麼做的目的是為了包容開發者的錯誤,簡化 web 開發,例如能省略一些起始或者結束标記等等)

說了那麼多,我們來回顧一下:

浏覽器向伺服器發起 HTTP 請求

伺服器響應請求,并傳回網頁資料

浏覽器通過标記化将響應資料(位元組)轉換為 tokens

浏覽器将 tokens 轉換為節點

浏覽器将節點插入 DOM 樹

等待建構 CSSOM 樹

我們已經深入了解了不少浏覽器的工作原理,那麼接下來我們來看看一些更常見的開發痛點吧。首先說說優先級。

簡單來說,CSS 的優先級是指以正确的層疊順序應用規則。盡管可以使用多種 CSS 選擇器來選中特定的标簽,浏覽器仍需要一種方式來決定最終哪些樣式将會生效。在決策過程中,首先浏覽器會計算每個選擇器的優先級。

不幸的是,優先級的計算規則難倒了不少 JavaScript 開發者,是以讓我們一起深入研究 CSS 優先級的計算規則。我們将使用以下的 html 結構作為例子:有一個類名為 <code>container</code> 的 div,在這個 div 裡,我們嵌套了另一個 div,它的 id 是 <code>main</code>,我們又在這個 div 裡嵌套了一個包含 a 标簽的 p 标簽。别偷看答案,你知道 a 标簽的顔色是什麼嗎?

(譯注:加一段 html 結構順便防偷看答案 →_→)

答案是粉色,它的優先級為:1,1,1。以下是其餘選擇器的優先級:

<code>div #main p a: 1,0,3</code>

<code>#main a: 1,0,1</code>

<code>p a: 2</code>

<code>a: 1</code>

優先級的每一個數的計算規則如下:

第一個數:ID 選擇器的數量

第二個數:類選擇器、屬性選擇器(不包含:<code>[type="text"]</code>, <code>[rel="nofollow"]</code>)、以及僞類選擇器(不包含:<code>:hover</code>, <code>:visited</code>)的數量和。

第三個數:元素選擇器與僞元素選擇器(不包含: <code>::before</code>, <code>::after</code>)的數量和。

是以,對于以下選擇器:

該選擇器的優先級是:1,2,2。因為我們有 1 個 ID 選擇器、1 個類選擇器、1 個僞類選擇器、還有 2 個元素選擇器(<code>li</code>、<code>a</code>)。你可以把優先級看作一個數字,比如 1,2,2 就是 122。這裡的逗号是為了提現你優先級的數值并不是以 10 進制計算的。理論上你可以讓一個元素的優先級為:0,1,13,4,其中的 13 并不會像 10 進制那樣産生進位。(譯注:不會變成 0,2,3,4)

其次,我想花點時間讨論一下定位。正如前文所說的,定位和布局是密切相關的。

布局是一個遞歸的過程,當全局樣式變化的時候,有時會在整個渲染樹上(重新)觸釋出局,有時則僅在局部變化的地方增量更新。有一件有趣的事情值得注意:如果我們重新思考渲染樹中的絕對定位元素,該對象在渲染樹中的位置和它在 DOM 樹中的位置不同的。

我也經常被問及應該使用 <code>flexbox</code> 還是 <code>float</code> 進行布局。毫無疑問,用 <code>flexbox</code> 進行布局相當友善,而且當應用于同一個元素時,<code>flexbox</code> 布局将在大約 3.5ms 内呈現,而 <code>float</code>布局可能需要大約 14ms。是以,磨砺你的 CSS 技能所帶來的回報不下于磨砺你的 JavaScript 技能的回報。

最後,我想聊聊 <code>z-index</code>。起初 <code>z-index</code> 聽起來很簡單。HTML 文檔中的每個元素都可以處在文檔的每個其他元素的前面或後面。 而它也隻适用于指定了定位方式的元素(譯注:即,未被定位,非 <code>position:static</code> 的元素)。如果你嘗試在沒有被定位的元素上設定 <code>z-index</code>,則不會起作用。

調試 z-index 問題的關鍵是了解層疊上下文,并始終從層疊上下文的根元素開始調試。 層疊上下文是 HTML 元素的三維概念,這些 HTML 元素在一條假想的相對于面向視窗(電腦螢幕)的使用者的 z 軸上延伸。換句話說,它是一組具有相同父級的元素,在同一個層疊上下文領域,層疊水準值大的那一個覆寫小的那一個。

每個層疊上下文都有一個唯一的 HTML 元素作為其根元素,并且在不涉及 <code>z-index</code> 和<code>position</code> 屬性時,層疊規則很簡單:層疊順序與元素在 HTML 中出現的順序相同。(譯注:即,新繪制的元素會覆寫之前的元素)

當然,你也可以使用 <code>z-index</code> 之外的屬性來建立新的層疊上下文,這會導緻情況更為複雜。以下屬性都會建立新的層疊上下文:

<code>opacity</code> 值不是 1

<code>filter</code> 值不是 <code>none</code>

<code>mix-blend-mode</code> 值不是 <code>normal</code>

順便提一下,blend mode 決定了指定圖層上的像素與其下方圖層上的可見像素的混合方式。

<code>transform</code> 屬性值不為 <code>none</code> 的元素同樣會建立新的層疊上下文。例如 <code>scale(1)</code> 和<code>translate3d(0,0,0)</code>。同樣順便提一下,<code>scale</code> 屬性是用于調整元素大小的,而<code>translate3d</code> 屬性則會啟用 GPU 加速讓 CSS 動畫更為流暢 。

<b>原文釋出時間為:2017年7月25日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀