在上一篇文章中我們介紹了導航相關的流程,那導航被提交后又會怎么樣呢?就進入了渲染階段。這個階段很重要,了解其相關流程能讓你“看透”頁面是如何工作的,有了這些知識,你可以解決一系列相關的問題,比如能熟練使用開發者工具,因為能夠理解開發者工具里面大部分項目的含義,能優化頁面卡頓問題,使用JavaScript優化動畫流程,通過優化樣式表來防止強制同步布局,等等。
既然它的功能這么強大,那么今天,我們就來好好聊聊渲染流程。
通常,我們編寫好HTML、CSS、JavaScript等文件,經過瀏覽器就會顯示出漂亮的頁面(如下圖所示),但是你知道它們是如何轉化成頁面的嗎?這背后的原理,估計很多人都答不上來。
從圖中可以看出,左邊輸入的是HTML、CSS、JavaScript數據,這些數據經過中間渲染模塊的處理,最終輸出為屏幕上的像素。
這中間的渲染模塊就是我們今天要討論的主題。為了能更好地理解下文,你可以先結合下圖快速抓住HTML、CSS和JavaScript的含義:
從上圖可以看出,HTML的內容是由標記和文本組成。標記也稱為標簽,每個標簽都有它自己的語意,瀏覽器會根據標簽的語意來正確展示HTML內容。比如上面的<p>
標簽是告訴瀏覽器在這里的內容需要創建一個新段落,中間的文本就是段落中需要顯示的內容
如果需要改變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節點擁有正確的樣式,這就需要樣式計算了
#樣式計算
樣式計算的目的是為了計算出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元素的布局信息,使其都保存在布局樹中。