https://time.geekbang.org/column/article/118205
2.5 渲染流程(上):HTML、CSS和JavaScript,是如何變成頁面的?
2.4講了導航相關的流程,那導航被提交后又會怎么樣呢?
就進入了渲染階段。
這個階段很重要,了解其相關流程能讓你**“看透”頁面是如何工作的**,有了這些知識,你可以解決一系列相關的問題,比如能熟練使用開發者工具,因為能夠理解開發者工具里面大部分項目的含義,能優化頁面卡頓問題,使用 JavaScript 優化動畫流程,通過優化樣式表來防止強制同步布局,等等。
2.5節講渲染流程。通常,編寫好 HTML、CSS、JavaScript 等文件,經過瀏覽器就會顯示出漂亮的頁面(如下圖所示),但是你知道它們是如何轉化成頁面的嗎?
從圖中可以看出,左邊輸入的是 HTML、CSS、JavaScript 數據,這些數據經過中間渲染模塊的處理,最終輸出為屏幕上的像素。
這中間的渲染模塊就是我們今天要討論的主題。為了能更好地理解下文,可以先結合下圖快速抓住 HTML、CSS 和 JavaScript 的含義:
-
HTML 的內容是由標記和文本組成。標記也稱為標簽,每個標簽都有它自己的語義,瀏覽器會根據標簽的語義來正確展示 HTML 內容。
比如上面的標簽是告訴瀏覽器在這里的內容需要創建一個新段落,中間的文本就是段落中需要顯示的內容。
-
如果需要改變 HTML 的字體顏色、大小等信息,就需要用到 CSS。CSS 又稱為層疊樣式表,是由選擇器和屬性組成.
比如圖中的 p 選擇器,它會把 HTML 里面
<p>
標簽的內容選擇出來,然后再把選擇器的屬性值應用到<p>
標簽內容上。選擇器里面有個 color 屬性,它的值是 red,這是告訴渲染引擎把<p>
標簽的內容顯示為紅色。 -
至于 JavaScript(簡稱為 JS),使用它可以使網頁的內容“動”起來。
比如上圖中,可以通過 JavaScript 來修改 CSS 樣式值,從而達到修改文本顏色的目的。
搞清楚 HTML、CSS 和 JavaScript 的含義后,那么接下來我們就正式開始分析渲染模塊了。
由于渲染機制過于復雜,所以渲染模塊在執行過程中會被劃分為很多子階段,輸入的 HTML 經過這些子階段,最后輸出像素。我們把這樣的一個處理流程叫做渲染流水線,其大致流程如下圖所示:
按照渲染的時間順序,流水線可分為如下幾個子階段:構建 DOM 樹、樣式計算、布局階段、分層、繪制、分塊、光柵化和合成。內容比較多,我會用兩篇文章來為你詳細講解這各個子階段。接下來,在介紹每個階段的過程中,你應該重點關注以下三點內容:
- 開始每個子階段都有其輸入的內容;
- 然后每個子階段有其處理過程;
- 最終每個子階段會生成輸出內容。
理解了這三部分內容,能讓你更加清晰地理解每個子階段。
構建 DOM 樹
為什么要構建 DOM 樹呢?這是因為瀏覽器無法直接理解和使用 HTML,所以需要將 HTML 轉換為瀏覽器能夠理解的結構——DOM 樹。
這里我們還需要簡單介紹下什么是樹結構,為了更直觀地理解,你可以參考下面我畫的幾個樹結構:
樹這種結構非常像我們現實生活中的“樹”,其中每個點我們稱為節點,相連的節點稱為父子節點。樹結構在瀏覽器中的應用還是比較多的,比如下面我們要介紹的渲染流程,就在頻繁地使用樹結構。
接下來咱們還是言歸正傳,來看看 DOM 樹的構建過程,你可以參考下圖:
從圖中可以看出,構建 DOM 樹的輸入內容是一個非常簡單的 HTML 文件,然后經由 HTML 解析器解析,最終輸出樹狀結構的 DOM。
為了更加直觀地理解 DOM 樹,你可以打開 Chrome 的“開發者工具”,選擇**“Console”標簽來打開控制臺,然后在控制臺里面輸入“document”后回車**,這樣你就能看到一個完整的 DOM 樹結構,如下圖所示:
圖中的 document 就是 DOM 結構,你可以看到,DOM 和 HTML 內容幾乎是一樣的,但是和 HTML 不同的是,DOM 是保存在內存中樹狀結構,可以通過 JavaScript 來查詢或修改其內容。
那下面就來看看如何通過 JavaScript 來修改 DOM 的內容,在控制臺中輸入:
document.getElementsByTagName("p")[0].innerText = "black"
這行代碼的作用是把第一個<p>
標簽的內容修改為 black,具體執行結果你可以參考下圖:
從圖中可以看出,在執行了一段修改第一個<p>
標簽的 JavaScript 代碼后,DOM 的第一個 p 節點的內容成功被修改,同時頁面中的內容也被修改了。
好了,現在已經生成 DOM 樹了,但是 DOM 節點的樣式我們依然不知道,要讓 DOM 節點擁有正確的樣式,這就需要樣式計算了。
樣式計算(Recalculate Style)
樣式計算的目的是為了計算出 DOM 節點中每個元素的具體樣式,這個階段大體可分為三步來完成。
1. 把 CSS 轉換為瀏覽器能夠理解的結構
那 CSS 樣式的來源主要有哪些呢?你可以先參考下圖:
從圖中可以看出,CSS 樣式來源主要有三種:
- 通過 link 引用的外部 CSS 文件
<style>
標記內的 CSS- 元素的 style 屬性內嵌的 CSS
和 HTML 文件一樣,瀏覽器也是無法直接理解這些純文本的 CSS 樣式,所以當渲染引擎接收到 CSS 文本時,會執行一個轉換操作,將 CSS 文本轉換為瀏覽器可以理解的結構——styleSheets。
為了加深理解,你可以在 Chrome 控制臺中查看其結構,只需要在控制臺中輸入 document.styleSheets,然后就看到如下圖所示的結構:
從圖中可以看出,這個樣式表包含了很多種樣式,已經把那三種來源的樣式都包含進去了。當然樣式表的具體結構不是我們今天討論的重點,你只需要知道渲染引擎會把獲取到的 CSS 文本全部轉換為 styleSheets 結構中的數據,并且該結構同時具備了查詢和修改功能,這會為后面的樣式操作提供基礎。
2. 轉換樣式表中的屬性值,使其標準化
現在我們已經把現有的 CSS 文本轉化為瀏覽器可以理解的結構了,那么接下來就要對其進行屬性值的標準化操作。
要理解什么是屬性值標準化,你可以看下面這樣一段 CSS 文本:
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
可以看到上面的 CSS 文本中有很多屬性值,如 2em、blue、bold,這些類型數值不容易被渲染引擎理解,所以需要將所有值轉換為渲染引擎容易理解的、標準化的計算值,這個過程就是屬性值標準化。
那標準化后的屬性值是什么樣子的?
從圖中可以看到,2em 被解析成了 32px,red 被解析成了 rgb(255,0,0),bold 被解析成了 700……
3. 計算出 DOM 樹中每個節點的具體樣式
現在樣式的屬性已被標準化了,接下來就需要計算 DOM 樹中每個節點的樣式屬性了,如何計算呢?
這就涉及到 CSS 的繼承規則和層疊規則了。首先是 CSS 繼承。CSS 繼承就是每個 DOM 節點都包含有父節點的樣式。這么說可能有點抽象,我們可以結合具體例子,看下面這樣一張樣式表是如何應用到 DOM 節點上的。
body { font-size: 20px }
p {color:blue;}
span {display: none}
div {font-weight: bold;color:red}
div p {color:green;}
這張樣式表最終應用到 DOM 節點的效果如下圖所示:
從圖中可以看出,所有子節點都繼承了父節點樣式。比如 body 節點的 font-size 屬性是 20,那 body 節點下面的所有節點的 font-size 都等于 20。
為了加深你對 CSS 繼承的理解,你可以打開 Chrome 的“開發者工具”,選擇第一個“element”標簽,再選擇“style”子標簽,你會看到如下界面:
這個界面展示的信息很豐富,大致可描述為如下。
-
首先,可以選擇要查看的元素的樣式(位于圖中的區域 2 中),在圖中的第 1 個區域中點擊對應的元素,就可以在下面的區域查看該元素的樣式了
比如這里我們選擇的元素是
<p>
標簽,位于 html.body.div. 這個路徑下面。 -
其次,可以從樣式來源(位于圖中的區域 3 中)中查看樣式的具體來源信息,看看是來源于樣式文件,還是來源于 UserAgent 樣式表。
這里需要特別提下 UserAgent 樣式,它是瀏覽器提供的一組默認樣式,如果你不提供任何樣式,默認使用的就是 UserAgent 樣式。
-
最后,可以通過區域 2 和區域 3 來查看樣式繼承的具體過程。
以上就是 CSS 繼承的一些特性,樣式計算過程中,會根據 DOM 節點的繼承關系來合理計算節點樣式。
樣式計算過程中的第二個規則是樣式層疊。層疊是 CSS 的一個基本特征,它是一個定義了如何合并來自多個源的屬性值的算法。它在 CSS 處于核心地位,CSS 的全稱“層疊樣式表”正是強調了這一點。
關于層疊的具體規則這里就不做過多介紹了,網上資料也非常多,你可以自行搜索學習。總之,樣式計算階段的目的是為了計算出 DOM 節點中每個元素的具體樣式,在計算過程中需要遵守 CSS 的繼承和層疊兩個規則。這個階段最終輸出的內容是每個 DOM 節點的樣式,并被保存在 ComputedStyle 的結構內。
如果你想了解每個 DOM 元素最終的計算樣式,可以打開 Chrome 的“開發者工具”,選擇第一個“element”標簽,然后再選擇“Computed”子標簽,如下圖所示:
上圖紅色方框中顯示了 html.body.div.p 標簽的 ComputedStyle 的值。你想要查看哪個元素,點擊左邊對應的標簽就可以了。
布局階段
現在,我們有 DOM 樹和 DOM 樹中元素的樣式,但這還不足以顯示頁面,因為我們還不知道 DOM 元素的幾何位置信息。
那么接下來就需要計算出 DOM 樹中可見元素的幾何位置,我們把這個計算過程叫做布局。
Chrome 在布局階段需要完成兩個任務:創建布局樹和布局計算。
1 創建布局樹
你可能注意到了 DOM 樹還含有很多不可見的元素,比如 head 標簽,還有使用了 display:none 屬性的元素。所以在顯示之前,我們還要額外地構建一棵只包含可見元素布局樹。
我們結合下圖來看看布局樹的構造過程:
從上圖可以看出,DOM 樹中所有不可見的節點都沒有包含到布局樹中。
為了構建布局樹,瀏覽器大體上完成了下面這些工作:
- 遍歷 DOM 樹中的所有可見節點,并把這些節點加到布局樹中;
- 而不可見的節點會被布局樹忽略掉,
如 head 標簽下面的全部內容,再比如 body.p.span 這個元素,因為它的屬性包含 dispaly:none,所以這個元素也沒有被包進布局樹。
2. 布局計算
現在我們有了一棵完整的布局樹。那么接下來,就要計算布局樹節點的坐標位置了。布局的計算過程非常復雜,我們這里先跳過不講,等到后面章節中我再做詳細的介紹。
在執行布局操作的時候,會把布局運算的結果重新寫回布局樹中,所以布局樹既是輸入內容也是輸出內容,這是布局階段一個不合理的地方,因為在布局階段并沒有清晰地將輸入內容和輸出內容區分開來。針對這個問題,Chrome 團隊正在重構布局代碼,下一代布局系統叫 LayoutNG,試圖更清晰地分離輸入和輸出,從而讓新設計的布局算法更加簡單。
總結
從圖中可以看出,本節內容我們介紹了渲染流程的前三個階段:DOM 生成、樣式計算和布局。要點可大致總結為如下:
- 瀏覽器不能直接理解 HTML 數據,所以第一步需要將其轉換為瀏覽器能夠理解的 DOM 樹結構;
- 生成 DOM 樹后,還需要根據 CSS 樣式表,來計算出 DOM 樹所有節點的樣式;
- 最后計算 DOM 元素的布局信息,使其都保存在布局樹中。
到這里我們的每個節點都擁有了自己的樣式和布局信息,那么后面幾個階段就要利用這些信息去展示頁面了,由于篇幅限制,剩下的這些階段我會在下一篇文章中介紹。
思考時間
最后,給你留個思考題:如果下載 CSS 文件阻塞了,會阻塞 DOM 樹的合成嗎?會阻塞頁面的顯示嗎?
好問題。