學習瀏覽器的內部工作原理將有助于您作出更明智的決策,并理解那些最佳開發實踐的個中緣由
瀏覽器的主要功能
瀏覽器的主要功能就是向服務器發出請求,在瀏覽器窗口中展示您選擇的網絡資源。這里所說的資源一般是指 html 文檔,也可以是 PDF、圖片或其他的類型。資源的位置由用戶使用 URI(統一資源標示符)指定。
瀏覽器解釋并顯示 HTML 文件的方式是在 HTML 和 css 規范中指定的。這些規范由網絡標準化組織 W3C(萬維網聯盟)進行維護。
多年以來,各瀏覽器都沒有完全遵從這些規范,同時還在開發自己獨有的擴展程序,這給網絡開發人員帶來了嚴重的兼容性問題。如今,大多數的瀏覽器都是或多或少地遵從規范。
瀏覽器的用戶界面有很多彼此相同的元素,其中包括:
- 用來輸入 URI 的地址欄
- 前進和后退按鈕
- 書簽設置選項
- 用于刷新和停止加載當前文檔的刷新和停止按鈕
- 用于返回主頁的主頁按鈕
奇怪的是,瀏覽器的用戶界面并沒有任何正式的規范,這是多年來的最佳實踐自然發展以及彼此之間相互模仿的結果。
html5也沒有定義瀏覽器必須具有的用戶界面元素,但列出了一些通用的元素,例如地址欄、狀態欄和工具欄等。當然,各瀏覽器也可以有自己獨特的功能,比如
Firefox 的下載管理器。
瀏覽器的高層結構
瀏覽器的主要組件為 :
- 用戶界面 – 包括地址欄、前進/后退按鈕、書簽菜單等。除了瀏覽器主窗口顯示的您請求的頁面外,其他顯示的各個部分都屬于用戶界面。
- 瀏覽器引擎 – 在用戶界面和呈現引擎之間傳送指令。
- 呈現引擎 – 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,并將解析后的內容顯示在屏幕上。
- 網絡 – 用于網絡調用,比如 HTTP 請求。其接口與平臺無關,并為所有平臺提供底層實現。
- 用戶界面后端 – 用于繪制基本的窗口小部件,比如組合框和窗口。其公開了與平臺無關的通用接口,而在底層使用操作系統的用戶界面方法。
- javascript 解釋器。用于解析和執行 JavaScript 代碼。
- 數據存儲。這是持久層。瀏覽器需要在硬盤上保存各種數據,例如 Cookie。新的 HTML 規范 (HTML5) 定義了“網絡數據庫”,這是一個完整(但是輕便)的瀏覽器內數據庫。
圖:瀏覽器的主要組件。
值得注意的是,和大多數瀏覽器不同,Chrome 瀏覽器的每個標簽頁都分別對應一個呈現引擎實例。每個標簽頁都是一個獨立的進程。
呈現引擎
呈現引擎的作用嘛…當然就是“呈現”了,也就是在瀏覽器的屏幕上顯示請求的內容。
默認情況下,呈現引擎可顯示 HTML 和 XML 文檔與圖片。通過插件(或瀏覽器擴展程序),還可以顯示其它類型的內容;例如,使用 PDF 查看器插件就能顯示 PDF 文檔。但是在本章中,我們將集中介紹其主要用途:顯示使用 CSS 格式化的 HTML 內容和圖片。
本文所討論的瀏覽器(Firefox、Chrome 瀏覽器和 Safari)是基于兩種呈現引擎構建的。Firefox 使用的是 Gecko,這是 Mozilla 公司“自制”的呈現引擎。而 Safari 和 Chrome 瀏覽器使用的都是 Webkit。
Webkit 是一種開放源代碼呈現引擎,起初用于 Linux 平臺,隨后由 Apple 公司進行修改,從而支持蘋果機和 Windows。有關詳情,請參閱 webkit.org。
主流程
呈現引擎一開始會從網絡層獲取請求文檔的內容,內容的大小一般限制在 8000 個塊以內。
然后進行如下所示的基本流程:
圖:呈現引擎的基本流程。
呈現引擎將開始解析 HTML 文檔,并將各標記逐個轉化成“內容樹”上的 DOM 節點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數據。HTML 中這些帶有視覺指令的樣式信息將用于創建另一個樹結構:呈現樹。
呈現樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序。
呈現樹構建完畢之后,進入“布局”處理階段,也就是為每個節點分配一個應出現在屏幕上的確切坐標。下一個階段是繪制 – 呈現引擎會遍歷呈現樹,由用戶界面后端層將每個節點繪制出來。
需要著重指出的是,這是一個漸進的過程。為達到更好的用戶體驗,呈現引擎會力求盡快將內容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之后,就會開始構建呈現樹和設置布局。在不斷接收和處理來自網絡的其余內容的同時,呈現引擎會將部分內容解析并顯示出來。
主流程示例
圖:Webkit 主流程
圖:Mozilla 的 Gecko 呈現引擎主流程
從圖 3 和圖 4 可以看出,雖然 Webkit 和 Gecko 使用的術語略有不同,但整體流程是基本相同的。
Gecko 將視覺格式化元素組成的樹稱為“框架樹”。每個元素都是一個框架。Webkit 使用的術語是“呈現樹”,它由“呈現對象”組成。對于元素的放置,Webkit 使用的術語是“布局”,而 Gecko 稱之為“重排”。對于連接 DOM 節點和可視化信息從而創建呈現樹的過程,Webkit 使用的術語是“附加”。有一個細微的非語義差別,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱為“內容槽”的層,用于生成 DOM 元素。我們會逐一論述流程中的每一部分:
解析 – 綜述
解析是呈現引擎中非常重要的一個環節,因此我們要更深入地講解。首先,來介紹一下解析。
解析文檔是指將文檔轉化成為有意義的結構,也就是可讓代碼理解和使用的結構。解析得到的結果通常是代表了文檔結構的節點樹,它稱作解析樹或者語法樹。
示例 – 解析 2 + 3 – 1 這個表達式,會返回下面的樹:
圖:數學表達式樹節點
語法
解析是以文檔所遵循的語法規則(編寫文檔所用的語言或格式)為基礎的。所有可以解析的格式都必須對應確定的語法(由詞匯和語法規則構成)。這稱為與上下文無關的語法。人類語言并不屬于這樣的語言,因此無法用常規的解析技術進行解析。
解析器和詞法分析器的組合
解析的過程可以分成兩個子過程:詞法分析和語法分析。
詞法分析是將輸入內容分割成大量標記的過程。標記是語言中的詞匯,即構成內容的單位。在人類語言中,它相當于語言字典中的單詞。
語法分析是應用語言的語法規則的過程。
解析器通常將解析工作分給以下兩個組件來處理:詞法分析器(有時也稱為標記生成器),負責將輸入內容分解成一個個有效標記;而解析器負責根據語言的語法規則分析文檔的結構,從而構建解析樹。詞法分析器知道如何將無關的字符(比如空格和換行符)分離出來。
圖:從源文檔到解析樹
解析是一個迭代的過程。通常,解析器會向詞法分析器請求一個新標記,并嘗試將其與某條語法規則進行匹配。如果發現了匹配規則,解析器會將一個對應于該標記的節點添加到解析樹中,然后繼續請求下一個標記。
如果沒有規則可以匹配,解析器就會將標記存儲到內部,并繼續請求標記,直至找到可與所有內部存儲的標記匹配的規則。如果找不到任何匹配規則,解析器就會引發一個異常。這意味著文檔無效,包含語法錯誤。
翻譯
很多時候,解析樹還不是最終產品。解析通常是在翻譯過程中使用的,而翻譯是指將輸入文檔轉換成另一種格式。編譯就是這樣一個例子。編譯器可將源代碼編譯成機器代碼,具體過程是首先將源代碼解析成解析樹,然后將解析樹翻譯成機器代碼文檔。
圖:編譯流程
解析示例
在圖 5 中,我們通過一個數學表達式建立了解析樹。現在,讓我們試著定義一個簡單的數學語言,用來演示解析的過程。
詞匯:我們用的語言可包含整數、加號和減號。
語法:
- 構成語言的語法單位是表達式、項和運算符。
- 我們用的語言可以包含任意數量的表達式。
- 表達式的定義是:一個“項”接一個“運算符”,然后再接一個“項”。
- 運算符是加號或減號。
- 項是一個整數或一個表達式。
讓我們分析一下 2 + 3 – 1。
匹配語法規則的第一個子串是 2,而根據第 5 條語法規則,這是一個項。匹配語法規則的第二個子串是 2
+ 3,而根據第 3 條規則(一個項接一個運算符,然后再接一個項),這是一個表達式。下一個匹配項已經到了輸入的結束。2 + 3 – 1 是一個表達式,因為我們已經知道 2
+ 3 是一個項,這樣就符合“一個項接一個運算符,然后再接一個項”的規則。2 + + 不與任何規則匹配,因此是無效的輸入。
詞匯和語法的正式定義
詞匯通常用正則表達式表示。
例如,我們的示例語言可以定義如下:
INTEGER :0|[1-9][0-9]* PLUS : + MINUS: -
正如您所看到的,這里用正則表達式給出了整數的定義。語法通常使用一種稱為 BNF 的格式來定義。我們的示例語言可以定義如下:
expression := term operation term operation := PLUS | MINUS term := INTEGER | expression
之前我們說過,如果語言的語法是與上下文無關的語法,就可以由常規解析器進行解析。與上下文無關的語法的直觀定義就是可以完全用 BNF 格式表達的語法。有關正式定義,請參閱關于與上下文無關的語法的維基百科文章。
解析器類型
有兩種基本類型的解析器:自上而下解析器和自下而上解析器。直觀地來說,自上而下的解析器從語法的高層結構出發,嘗試從中找到匹配的結構。而自下而上的解析器從低層規則出發,將輸入內容逐步轉化為語法規則,直至滿足高層規則。
讓我們來看看這兩種解析器會如何解析我們的示例:
自上而下的解析器會從高層的規則開始:首先將 2 + 3 標識為一個表達式,然后將 2 + 3 – 1 標識為一個表達式(標識表達式的過程涉及到匹配其他規則,但是起點是最高級別的規則)。
自下而上的解析器將掃描輸入內容,找到匹配的規則后,將匹配的輸入內容替換成規則。如此繼續替換,直到輸入內容的結尾。部分匹配的表達式保存在解析器的堆棧中。
堆棧 | 輸入 |
---|---|
2 + 3 – 1 | |
項 | + 3 – 1 |
項運算 | 3 – 1 |
表達式 | - 1 |
表達式運算符 | 1 |
表達式 |
這種自下而上的解析器稱為移位歸約解析器,因為輸入在向右移位(設想有一個指針從輸入內容的開頭移動到結尾),并且逐漸歸約到語法規則上。
自動生成解析器
有一些工具可以幫助您生成解析器,它們稱為解析器生成器。您只要向其提供您所用語言的語法(詞匯和語法規則),它就會生成相應的解析器。創建解析器需要對解析有深刻理解,而人工創建優化的解析器并不是一件容易的事情,所以解析器生成器是非常實用的。
Webkit 使用了兩種非常有名的解析器生成器:用于創建詞法分析器的 Flex 以及用于創建解析器的 Bison(您也可能遇到 Lex 和 Yacc 這樣的別名)。Flex 的輸入是包含標記的正則表達式定義的文件。Bison 的輸入是采用 BNF 格式的語言語法規則。
HTML 解析器
HTML 解析器的任務是將 HTML 標記解析成解析樹。
HTML 語法定義
HTML 的詞匯和語法在 W3C 組織創建的規范中進行了定義。當前的版本是 HTML4,HTML5 正在處理過程中。
非與上下文無關的語法
正如我們在解析過程的簡介中已經了解到的,語法可以用 BNF 等格式進行正式定義。
很遺憾,所有的常規解析器都不適用于 HTML(我并不是開玩笑,它們可以用于解析 CSS 和 JavaScript)。HTML 并不能很容易地用解析器所需的與上下文無關的語法來定義。
有一種可以定義 HTML 的正規格式:DTD(Document Type Definition,文檔類型定義),但它不是與上下文無關的語法。
這初看起來很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一個 XML 變體 (XHTML),那么有什么大的區別呢?
區別在于 HTML 的處理更為“寬容”,它允許您省略某些隱式添加的標記,有時還能省略一些起始或者結束標記等等。和 XML 嚴格的語法不同,HTML 整體來看是一種“軟性”的語法。
顯然,這種看上去細微的差別實際上卻帶來了巨大的影響。一方面,這是 HTML 如此流行的原因:它能包容您的錯誤,簡化網絡開發。另一方面,這使得它很難編寫正式的語法。概括地說,HTML 無法很容易地通過常規解析器解析(因為它的語法不是與上下文無關的語法),也無法通過 XML 解析器來解析。
HTML DTD
HTML 的定義采用了 DTD 格式。此格式可用于定義 SGML 族的語言。它包括所有允許使用的元素及其屬性和層次結構的定義。如上文所述,HTML DTD 無法構成與上下文無關的語法。
DTD 存在一些變體。嚴格模式完全遵守 HTML 規范,而其他模式可支持以前的瀏覽器所使用的標記。這樣做的目的是確保向下兼容一些早期版本的內容。最新的嚴格模式 DTD 可以在這里找到:www.w3.org/TR/html4/strict.dtd
DOM
解析器的輸出“解析樹”是由 DOM 元素和屬性節點構成的樹結構。DOM 是文檔對象模型 (Document Object Model) 的縮寫。它是 HTML 文檔的對象表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的接口。
解析樹的根節點是“Document”對象。
DOM 與標記之間幾乎是一一對應的關系。比如下面這段標記:
<html> <body> <p> Hello World </p> <div> <img src="example.png"/></div> </body> </html>
可翻譯成如下的 DOM 樹:
圖:示例標記的 DOM 樹
和 HTML 一樣,DOM 也是由 W3C 組織指定的。請參見 www.w3.org/DOM/DOMTR。這是關于文檔操作的通用規范。其中一個特定模塊描述針對 HTML 的元素。HTML 的定義可以在這里找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
我所說的樹包含 DOM 節點,指的是樹是由實現了某個 DOM 接口的元素構成的。瀏覽器所用的具體實現也會具有一些其他屬性,供瀏覽器在內部使用。
解析算法
我們在之前章節已經說過,HTML 無法用常規的自上而下或自下而上的解析器進行解析。
原因在于:
- 語言的寬容本質。
- 瀏覽器歷來對一些常見的無效 HTML 用法采取包容態度。
-
解析過程需要不斷地反復。源內容在解析過程中通常不會改變,但是在 HTML 中,腳本標記如果包含
document.write
,就會添加額外的標記,這樣解析過程實際上就更改了輸入內容。
由于不能使用常規的解析技術,瀏覽器就創建了自定義的解析器來解析 HTML。
HTML5 規范詳細地描述了解析算法。此算法由兩個階段組成:標記化和樹構建。
標記化是詞法分析過程,將輸入內容解析成多個標記。HTML 標記包括起始標記、結束標記、屬性名稱和屬性值。
標記生成器識別標記,傳遞給樹構造器,然后接受下一個字符以識別下一個標記;如此反復直到輸入的結束。
圖:HTML 解析流程(摘自 HTML5 規范)
標記化算法
該算法的輸出結果是 HTML 標記。該算法使用狀態機來表示。每一個狀態接收來自輸入信息流的一個或多個字符,并根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入 下一狀態的決定。這意味著,即使接收的字符相同,對于下一個正確的狀態也會產生不同的結果,具體取決于當前的狀態。該算法相當復雜,無法在此詳述,所以我 們通過一個簡單的示例來幫助大家理解其原理。
基本示例 – 將下面的 HTML 代碼標記化:
<html> <body> Hello world </body> </html>
初始狀態是數據狀態。遇到字符 <
時,狀態更改為“標記打開狀態”。接收一個 a-z
字符會創建“起始標記”,狀態更改為“標記名稱狀態”。這個狀態會一直保持到接收 >
字符。在此期間接收的每個字符都會附加到新的標記名稱上。在本例中,我們創建的標記是 html
標記。
遇到 >
標記時,會發送當前的標記,狀態改回“數據狀態”。<body>
標記也會進行同樣的處理。目前 html
和 body
標記均已發出。現在我們回到“數據狀態”。接收到 Hello
world
中的 H
字符時,將創建并發送字符標記,直到接收 </body>
中的 <
。我們將為 Hello
world
中的每個字符都發送一個字符標記。
現在我們回到“標記打開狀態”。接收下一個輸入字符 /
時,會創建 end
tag token
并改為“標記名稱狀態”。我們會再次保持這個狀態,直到接收 >
。然后將發送新的標記,并回到“數據狀態”。</html>
輸入也會進行同樣的處理。
圖:對示例輸入進行標記化
樹構建算法
在創建解析器的同時,也會創建 Document 對象。在樹構建階段,以 Document 為根節點的 DOM 樹也會不斷進行修改,向其中添加各種元素。標記生成器發送的每個節點都會由樹構建器進行處理。規范中定義了每個標記所對應的 DOM 元素,這些元素會在接收到相應的標記時創建。這些元素不僅會添加到 DOM 樹中,還會添加到開放元素的堆棧中。此堆棧用于糾正嵌套錯誤和處理未關閉的標記。其算法也可以用狀態機來描述。這些狀態稱為“插入模式”。
讓我們來看看示例輸入的樹構建過程:
<html> <body> Hello world </body> </html>
樹構建階段的輸入是一個來自標記化階段的標記序列。第一個模式是“initial mode”。接收 HTML 標記后轉為“before html”模式,并在這個模式下重新處理此標記。這樣會創建一個 HTMLHtmlElement 元素,并將其附加到 Document 根對象上。
然后狀態將改為“before head”。此時我們接收“body”標記。即使我們的示例中沒有“head”標記,系統也會隱式創建一個 HTMLHeadElement,并將其添加到樹中。
現在我們進入了“in head”模式,然后轉入“after head”模式。系統對 body 標記進行重新處理,創建并插入 HTMLBodyElement,同時模式轉變為“body”。
現在,接收由“Hello world”字符串生成的一系列字符標記。接收第一個字符時會創建并插入“Text”節點,而其他字符也將附加到該節點。
接收 body 結束標記會觸發“after body”模式。現在我們將接收 HTML 結束標記,然后進入“after after body”模式。接收到文件結束標記后,解析過程就此結束。
圖:示例 HTML 的樹構建
解析結束后的操作
在此階段,瀏覽器會將文檔標注為交互狀態,并開始解析那些處于“deferred”模式的腳本,也就是那些應在文檔解析完成后才執行的腳本。然后,文檔狀態將設置為“完成”,一個“加載”事件將隨之觸發。
瀏覽器的容錯機制
您在瀏覽 HTML 網頁時從來不會看到“語法無效”的錯誤。這是因為瀏覽器會糾正任何無效內容,然后繼續工作。
以下面的 HTML 代碼為例:
<html> <mytag> </mytag> <div> <p> </div> Really lousy HTML </p> </html>
在這里,我已經違反了很多語法規則(“mytag”不是標準的標記,“p”和“div”元素之間的嵌套有誤等等),但是瀏覽器仍然會正確地顯示這些內容,并且毫無怨言。因為有大量的解析器代碼會糾正 HTML 網頁作者的錯誤。
不同瀏覽器的錯誤處理機制相當一致,但令人稱奇的是,這種機制并不是 HTML 當前規范的一部分。和書簽管理以及前進/后退按鈕一樣,它也是瀏覽器在多年發展中的產物。很多網站都普遍存在著一些已知的無效 HTML 結構,每一種瀏覽器都會嘗試通過和其他瀏覽器一樣的方式來修復這些無效結構。
HTML5 規范定義了一部分這樣的要求。Webkit 在 HTML 解析器類的開頭注釋中對此做了很好的概括。
解析器對標記化輸入內容進行解析,以構建文檔樹。如果文檔的格式正確,就直接進行解析。
遺憾的是,我們不得不處理很多格式錯誤的 HTML 文檔,所以解析器必須具備一定的容錯性。
我們至少要能夠處理以下錯誤情況:
- 明顯不能在某些外部標記中添加的元素。在此情況下,我們應該關閉所有標記,直到出現禁止添加的元素,然后再加入該元素。
- 我們不能直接添加的元素。這很可能是網頁作者忘記添加了其中的一些標記(或者其中的標記是可選的)。這些標簽可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
- 向 inline 元素內添加 block 元素。關閉所有 inline 元素,直到出現下一個較高級的 block 元素。
- 如果這樣仍然無效,可關閉所有元素,直到可以添加元素為止,或者忽略該標記。
讓我們看一些 Webkit 容錯的示例:
使用了 </BR> 而不是 <BR>
有些網站使用了 </br> 而不是 <br>。為了與 IE 和 Firefox 兼容,Webkit 將其與 <br> 做同樣的處理。
代碼如下:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true; }
請注意,錯誤處理是在內部進行的,用戶并不會看到這個過程。
離散表格
離散表格是指位于其他表格內容中,但又不在任何一個單元格內的表格。
比如以下的示例:
<table> <table> <tr><td>inner table</td></tr> </table> <tr><td>outer table</td></tr> </table>
Webkit 會將其層次結構更改為兩個同級表格:
<table> <tr><td>outer table</td></tr> </table> <table> <tr><td>inner table</td></tr> </table>
代碼如下:
if (m_inStrayTableContent && localName == tableTag) popBlock(tableTag);
Webkit 使用一個堆棧來保存當前的元素內容,它會從外部表格的堆棧中彈出內部表格。現在,這兩個表格就變成了同級關系。
嵌套的表單元素
如果用戶在一個表單元素中又放入了另一個表單,那么第二個表單將被忽略。
代碼如下:
if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document); }
過于復雜的標記層次結構
代碼的注釋已經說得很清楚了。
示例網站 www.liceo.edu.mx 嵌套了約 1500 個標記,全都來自一堆 <b> 標記。我們只允許最多 20 層同類型標記的嵌套,如果再嵌套更多,就會全部忽略。
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName) { unsigned i = 0; for (HTMLStackElem* curr = m_blockStack; i < cMaxRedundantTagDepth && curr && curr->tagName == tagName; curr = curr->next, i++) { } return i != cMaxRedundantTagDepth; }
放錯位置的 HTML 或者 BODY 結束標記
同樣,代碼的注釋已經說得很清楚了。
支持格式非常糟糕的 HTML 代碼。我們從不關閉 body 標記,因為一些愚蠢的網頁會在實際文檔結束之前就關閉。我們通過調用 end() 來執行關閉操作。
if (t->tagName == htmlTag || t->tagName == bodyTag ) return;
所以網頁作者需要注意,除非您想作為反面教材出現在 Webkit 容錯代碼段的示例中,否則還請編寫格式正確的 HTML 代碼。
CSS 解析
還記得簡介中解析的概念嗎?和 HTML 不同,CSS 是上下文無關的語法,可以使用簡介中描述的各種解析器進行解析。事實上,CSS 規范定義了 CSS 的詞法和語法。
讓我們來看一些示例:
詞法語法(詞匯)是針對各個標記用正則表達式定義的:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/ num [0-9]+|[0-9]*"."[0-9]+ nonascii [\200-\377] nmstart [_a-z]|{nonascii}|{escape} nmchar [_a-z0-9-]|{nonascii}|{escape} name {nmchar}+ ident {nmstart}{nmchar}*
“ident”是標識符 (identifier) 的縮寫,比如類名。“name”是元素的 ID(通過“#”來引用)。
語法是采用 BNF 格式描述的。
ruleset : selector [ ',' S* selector ]* '{' S* declaration [ ';' S* declaration ]* '}' S* ; selector : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]? ; simple_selector : element_name [ HASH | class | attrib | pseudo ]* | [ HASH | class | attrib | pseudo ]+ ; class : '.' IDENT ; element_name : IDENT | '*' ; attrib : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* [ IDENT | STRING ] S* ] ']' ; pseudo : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ] ;
解釋:這是一個規則集的結構:
div.error , a.error { color:red; font-weight:bold; }
div.error 和 a.error 是選擇器。大括號內的部分包含了由此規則集應用的規則。此結構的正式定義是這樣的:
ruleset : selector [ ',' S* selector ]* '{' S* declaration [ ';' S* declaration ]* '}' S* ;
這表示一個規則集就是一個選擇器,或者由逗號和空格(S 表示空格)分隔的多個(數量可選)選擇器。規則集包含了大括號,以及其中的一個或多個(數量可選)由分號分隔的聲明。“聲明”和“選擇器”將由下面的 BNF 格式定義。
Webkit CSS 解析器
Webkit 使用 Flex 和 Bison 解析器生成器,通過 CSS 語法文件自動創建解析器。正如我們之前在解析器簡介中所說,Bison 會創建自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。這兩種解析器都會將 CSS 文件解析成 StyleSheet 對象,且每個對象都包含 CSS 規則。CSS 規則對象則包含選擇器和聲明對象,以及其他與 CSS 語法對應的對象。
圖:解析 CSS
處理腳本和樣式表的順序
腳本
網絡的模型是同步的。網頁作者希望解析器遇到 <script> 標記時立即解析并執行腳本。文檔的解析將停止,直到腳本執行完畢。如果腳本是外部的,那么解析過程會停止,直到從網絡同步抓取資源完成后再繼續。此模型已 經使用了多年,也在 HTML4 和 HTML5 規范中進行了指定。作者也可以將腳本標注為“defer”,這樣它就不會停止文檔解析,而是等到解析結束才執行。HTML5 增加了一個選項,可將腳本標記為異步,以便由其他線程解析和執行。
預解析
Webkit 和 Firefox 都進行了這項優化。在執行腳本時,其他線程會解析文檔的其余部分,找出并加載需要通過網絡加載的其他資源。通過這種方式,資源可以在并行連接上加載,從而 提高總體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工作交由主解析器處理;預解析器只會解析外部資源(例如外部腳本、樣式表和圖片)的引用。
樣式表
另一方面,樣式表有著不同的模型。理論上來說,應用樣式表不會更改 DOM 樹,因此似乎沒有必要等待樣式表并停止文檔解析。但這涉及到一個問題,就是腳本在文檔解析階段會請求樣式信息。如果當時還沒有加載和解析樣式,腳本就會獲 得錯誤的回復,這樣顯然會產生很多問題。這看上去是一個非典型案例,但事實上非常普遍。Firefox 在樣式表加載和解析的過程中,會禁止所有腳本。而對于 Webkit 而言,僅當腳本嘗試訪問的樣式屬性可能受尚未加載的樣式表影響時,它才會禁止該腳本。
呈現樹構建
在 DOM 樹構建的同時,瀏覽器還會構建另一個樹結構:呈現樹。這是由可視化元素按照其顯示順序而組成的樹,也是文檔的可視化表示。它的作用是讓您按照正確的順序繪制內容。
Firefox 將呈現樹中的元素稱為“框架”。Webkit 使用的術語是呈現器或呈現對象。
呈現器知道如何布局并將自身及其子元素繪制出來。
Webkits RenderObject 類是所有呈現器的基類,其定義如下:
class RenderObject{ virtual void layout(); virtual void paint(PaintInfo); virtual void rect repaintRect(); Node* node; //the DOM node RenderStyle* style; // the computed style RenderLayer* containgLayer; //the containing z-index layer }
每一個呈現器都代表了一個矩形的區域,通常對應于相關節點的 CSS 框,這一點在 CSS2 規范中有所描述。它包含諸如寬度、高度和位置等幾何信息。
框的類型會受到與節點相關的“display”樣式屬性的影響(請參閱樣式計算章節)。下面這段 Webkit 代碼描述了根據 display 屬性的不同,針對同一個 DOM 節點應創建什么類型的呈現器。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style) { Document* doc = node->document(); RenderArena* arena = doc->renderArena(); ... RenderObject* o = 0; switch (style->display()) { case NONE: break; case INLINE: o = new (arena) RenderInline(node); break; case BLOCK: o = new (arena) RenderBlock(node); break; case INLINE_BLOCK: o = new (arena) RenderBlock(node); break; case LIST_ITEM: o = new (arena) RenderListItem(node); break; ... } return o; }
元素類型也是考慮因素之一,例如表單控件和表格都對應特殊的框架。
在 Webkit 中,如果一個元素需要創建特殊的呈現器,就會替換 createRenderer
方法。呈現器所指向的樣式對象中包含了一些和幾何無關的信息。
呈現樹和 DOM 樹的關系
呈現器是和 DOM 元素相對應的,但并非一一對應。非可視化的 DOM 元素不會插入呈現樹中,例如“head”元素。如果元素的 display 屬性值為“none”,那么也不會顯示在呈現樹中(但是 visibility 屬性值為“hidden”的元素仍會顯示)。有一些 DOM 元素對應多個可視化對象。它們往往是具有復雜結構的元素,無法用單一的矩形來描述。例如,“select”元素有 3 個呈現器:一個用于顯示區域,一個用于下拉列表框,還有一個用于按鈕。如果由于寬度不夠,文本無法在一行中顯示而分為多行,那么新的行也會作為新的呈現器
而添加。
另一個關于多呈現器的例子是格式無效的 HTML。根據 CSS 規范,inline 元素只能包含 block 元素或 inline 元素中的一種。如果出現了混合內容,則應創建匿名的 block 呈現器,以包裹 inline 元素。
有一些呈現對象對應于 DOM 節點,但在樹中所在的位置與 DOM 節點不同。浮動定位和絕對定位的元素就是這樣,它們處于正常的流程之外,放置在樹中的其他地方,并映射到真正的框架,而放在原位的是占位框架。
圖:呈現樹及其對應的 DOM 樹 (3.1)。初始容器 block 為“viewport”,而在 Webkit 中則為“RenderView”對象。
構建呈現樹的流程
在 Firefox 中,系統會針對 DOM 更新注冊展示層,作為偵聽器。展示層將框架創建工作委托給 FrameConstructor
,由該構造器解析樣式(請參閱樣式計算)并創建框架。
在 Webkit 中,解析樣式和創建呈現器的過程稱為“附加”。每個 DOM 節點都有一個“attach”方法。附加是同步進行的,將節點插入 DOM 樹需要調用新的節點“attach”方法。
處理 html 和 body 標記就會構建呈現樹根節點。這個根節點呈現對象對應于 CSS 規范中所說的容器 block,這是最上層的 block,包含了其他所有 block。它的尺寸就是視口,即瀏覽器窗口顯示區域的尺寸。Firefox 稱之為 ViewPortFrame
,而
Webkit 稱之為 RenderView
。這就是文檔所指向的呈現對象。呈現樹的其余部分以
DOM 樹節點插入的形式來構建。
請參閱關于處理模型的 CSS2 規范。
樣式計算
構建呈現樹時,需要計算每一個呈現對象的可視化屬性。這是通過計算每個元素的樣式屬性來完成的。
樣式包括來自各種來源的樣式表、inline 樣式元素和 HTML 中的可視化屬性(例如“bgcolor”屬性)。其中后者將經過轉化以匹配 CSS 樣式屬性。
樣式表的來源包括瀏覽器的默認樣式表、由網頁作者提供的樣式表以及由瀏覽器用戶提供的用戶樣式表(瀏覽器允許您定義自己喜歡的樣式。以 Firefox 為例,用戶可以將自己喜歡的樣式表放在“Firefox Profile”文件夾下)。
樣式計算存在以下難點:
- 樣式數據是一個超大的結構,存儲了無數的樣式屬性,這可能造成內存問題。
-
如果不進行優化,為每一個元素查找匹配的規則會造成性能問題。要為每一個元素遍歷整個規則列表來尋找匹配規則,這是一項浩大的工程。選擇器會具有很復雜的結構,這就會導致某個匹配過程一開始看起來很可能是正確的,但最終發現其實是徒勞的,必須嘗試其他匹配路徑。
例如下面這個組合選擇器:
div div div div{ ... }
這意味著規則適用于作為 3 個 div 元素的子代的
<div>
。如果您要檢查規則是否適用于某個指定的<div>
元素,應選擇樹上的一條向上路徑進行檢查。您可能需要向上遍歷節點樹,結果發現只有兩個 div,而且規則并不適用。然后,您必須嘗試樹中的其他路徑。 - 應用規則涉及到相當復雜的層疊規則(用于定義這些規則的層次)。
讓我們來看看瀏覽器是如何處理這些問題的:
共享樣式數據
Webkit 節點會引用樣式對象 (RenderStyle)。這些對象在某些情況下可以由不同節點共享。這些節點是同級關系,并且:
- 這些元素必須處于相同的鼠標狀態(例如,不允許其中一個是“:hover”狀態,而另一個不是)
- 任何元素都沒有 ID
- 標記名稱應匹配
- 類屬性應匹配
- 映射屬性的集合必須是完全相同的
- 鏈接狀態必須匹配
- 焦點狀態必須匹配
- 任何元素都不應受屬性選擇器的影響,這里所說的“影響”是指在選擇器中的任何位置有任何使用了屬性選擇器的選擇器匹配
- 元素中不能有任何 inline 樣式屬性
- 不能使用任何同級選擇器。WebCore 在遇到任何同級選擇器時,只會引發一個全局開關,并停用整個文檔的樣式共享(如果存在)。這包括 + 選擇器以及 :first-child 和 :last-child 等選擇器。
FIREFOX 規則樹
為了簡化樣式計算,Firefox 還采用了另外兩種樹:規則樹和樣式上下文樹。Webkit 也有樣式對象,但它們不是保存在類似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類對象的相關樣式。
圖:Firefox 樣式上下文樹 (2.2)
樣式上下文包含端值。要計算出這些值,應按照正確順序應用所有的匹配規則,并將其從邏輯值轉化為具體的值。例如,如果邏輯值是屏幕大小的百分比,則需要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間可以共享這些值,以避免重復計算,還可以節約空間。
所有匹配的規則都存儲在樹中。路徑中的底層節點擁有較高的優先級。規則樹包含了所有已知規則匹配的路徑。規則的存儲是延遲進行的。規則樹不會在開始的時候就為所有的節點進行計算,而是只有當某個節點樣式需要進行計算時,才會向規則樹添加計算的路徑。
這個想法相當于將規則樹路徑視為詞典中的單詞。如果我們已經計算出如下的規則樹:
假設我們需要為內容樹中的另一個元素匹配規則,并且找到匹配路徑是 B – E – I(按照此順序)。由于我們在樹中已經計算出了路徑 A – B – E – I – L,因此就已經有了此路徑,這就減少了現在所需的工作量。讓我們看看規則樹如何幫助我們減少工作。
結構劃分
樣式上下文可分割成多個結構。這些結構體包含了特定類別(如 border 或 color)的樣式信息。結構中的屬性都是繼承的或非繼承的。繼承屬性如果未由元素定義,則繼承自其父代。非繼承屬性(也稱為“重置”屬性)如果未進行定義,則使用默認值。
規則樹通過緩存整個結構(包含計算出的端值)為我們提供幫助。這一想法假定底層節點沒有提供結構的定義,則可使用上層節點中的緩存結構。
使用規則樹計算樣式上下文
在計算某個特定元素的樣式上下文時,我們首先計算規則樹中的對應路徑,或者使用現有的路徑。然后我們沿此路徑應用規則,在新的樣式上下文中填充結構。我們 從路徑中擁有最高優先級的底層節點(通常也是最特殊的選擇器)開始,并向上遍歷規則樹,直到結構填充完畢。如果該規則節點對于此結構沒有任何規范,那么我 們可以實現更好的優化:尋找路徑更上層的節點,找到后指定完整的規范并指向相關節點即可。這是最好的優化方法,因為整個結構都能共享。這可以減少端值的計 算量并節約內存。
如果我們找到了部分定義,就會向上遍歷規則樹,直到結構填充完畢。
如果我們找不到結構的任何定義,那么假如該結構是“繼承”類型,我們會在上下文樹中指向父代的結構,這樣也可以共享結構。如果是 reset 類型的結構,則會使用默認值。
如果最特殊的節點確實添加了值,那么我們需要另外進行一些計算,以便將這些值轉化成實際值。然后我們將結果緩存在樹節點中,供子代使用。
如果某個元素與其同級元素都指向同一個樹節點,那么它們就可以共享整個樣式上下文。
讓我們來看一個例子,假設我們有如下 HTML 代碼:
<html> <body> <div class="err" id="div1"> <p> this is a <span class="big"> big error </span> this is also a <span class="big"> very big error</span> error </p> </div> <div class="err" id="div2">another error</div> </body> </html>
還有如下規則:
- div {margin:5px;color:black}
- .err {color:red}
- .big {margin-top:3px}
- div span {margin-bottom:4px}
- #div1 {color:blue}
- #div2 {color:green}
為了簡便起見,我們只需要填充兩個結構:color 結構和 margin 結構。color 結構只包含一個成員(即“color”),而 margin 結構包含四條邊。
形成的規則樹如下圖所示(節點的標記方式為“節點名 : 指向的規則序號”):
圖:規則樹
上下文樹如下圖所示(節點名 : 指向的規則節點):
圖:上下文樹
假設我們解析 HTML 時遇到了第二個 <div> 標記,我們需要為此節點創建樣式上下文,并填充其樣式結構。
經過規則匹配,我們發現該 <div> 的匹配規則是第 1、2 和 6 條。這意味著規則樹中已有一條路徑可供我們的元素使用,我們只需要再為其添加一個節點以匹配第 6 條規則(規則樹中的 F 節點)。
我們將創建樣式上下文并將其放入上下文樹中。新的樣式上下文將指向規則樹中的 F 節點。
現在我們需要填充樣式結構。首先要填充的是 margin 結構。由于最后的規則節點 (F) 并沒有添加到 margin 結構,我們需要上溯規則樹,直至找到在先前節點插入中計算過的緩存結構,然后使用該結構。我們會在指定 margin 規則的最上層節點(即 B 節點)上找到該結構。
我們已經有了 color 結構的定義,因此不能使用緩存的結構。由于 color 有一個屬性,我們無需上溯規則樹以填充其他屬性。我們將計算端值(將字符串轉化為 RGB 等)并在此節點上緩存經過計算的結構。
第二個 <span> 元素處理起來更加簡單。我們將匹配規則,最終發現它和之前的 span 一樣指向規則 G。由于我們找到了指向同一節點的同級,就可以共享整個樣式上下文了,只需指向之前 span 的上下文即可。
對于包含了繼承自父代的規則的結構,緩存是在上下文樹中進行的(事實上 color 屬性是繼承的,但是 Firefox 將其視為 reset 屬性,并緩存到規則樹上)。
例如,如果我們在某個段落中添加 font 規則:
p {font-family:Verdana;font size:10px;font-weight:bold}
那么,該段落元素作為上下文樹中的 div 的子代,就會共享與其父代相同的 font 結構(前提是該段落沒有指定 font 規則)。在 Webkit 中沒有規則樹,因此會對匹配的聲明遍歷 4 次。首先應用非重要高優先級的屬性(由于作為其他屬性的依據而應首先應用的屬性,例如 display),接著是高優先級重要規則,然后是普通優先級非重要規則,最后是普通優先級重要規則。這意味著多次出現的屬性會根據正確的層疊順序進行解 析。最后出現的最終生效。
因此概括來說,共享樣式對象(整個對象或者對象中的部分結構)可以解決問題 1 和問題 3。Firefox 規則樹還有助于按照正確的順序應用屬性。
對規則進行處理以簡化匹配
樣式規則有一些來源:
-
外部樣式表或樣式元素中的 CSS 規則
p {color:blue}
-
inline 樣式屬性及類似內容
<p style="color:blue" />
-
HTML 可視化屬性(映射到相關的樣式規則)
<p bgcolor="blue" />
后兩種很容易和元素進行匹配,因為元素擁有樣式屬性,而且 HTML 屬性可以使用元素作為鍵值進行映射。
我們之前在第 2 個問題中提到過,CSS 規則匹配可能比較棘手。為了解決這一難題,可以對 CSS 規則進行一些處理,以便訪問。
樣式表解析完畢后,系統會根據選擇器將 CSS 規則添加到某個哈希表中。這些哈希表的選擇器各不相同,包括 ID、類名稱、標記名稱等,還有一種通用哈希表,適合不屬于上述類別的規則。如果選擇器是 ID,規則就會添加到 ID 表中;如果選擇器是類,規則就會添加到類表中,依此類推。
這種處理可以大大簡化規則匹配。我們無需查看每一條聲明,只要從哈希表中提取元素的相關規則即可。這種優化方法可排除掉 95% 以上規則,因此在匹配過程中根本就不用考慮這些規則了 (4.1)。
我們以如下的樣式規則為例:
p.error {color:red} #messageDiv {height:50px} div {margin:5px}
第一條規則將插入類表,第二條將插入 ID 表,而第三條將插入標記表。
對于下面的 HTML 代碼段:
<p class="error">an error occurred </p> <div id=" messageDiv">this is a message</div>
我們首先會為 p 元素尋找匹配的規則。類表中有一個“error”鍵,在下面可以找到“p.error”的規則。div 元素在 ID 表(鍵為 ID)和標記表中有相關的規則。剩下的工作就是找出哪些根據鍵提取的規則是真正匹配的了。
例如,如果 div 的對應規則如下:
table div {margin:5px}
這條規則仍然會從標記表中提取出來,因為鍵是最右邊的選擇器,但這條規則并不匹配我們的 div 元素,因為 div 沒有 table 祖先。Webkit 和 Firefox 都進行了這一處理。
以正確的層疊順序應用規則
樣式對象具有每個可視化屬性一一對應的屬性(均為 CSS 屬性但更為通用)。如果某個屬性未由任何匹配規則所定義,那么部分屬性就可由父代元素樣式對象繼承。其他屬性具有默認值。
如果定義不止一個,就會出現問題,需要通過層疊順序來解決。
樣式表層疊順序
某個樣式屬性的聲明可能會出現在多個樣式表中,也可能在同一個樣式表中出現多次。這意味著應用規則的順序極為重要。這稱為“層疊”順序。根據 CSS2 規范,層疊的順序為(優先級從低到高):
- 瀏覽器聲明
- 用戶普通聲明
- 作者普通聲明
- 作者重要聲明
- 用戶重要聲明
瀏覽器聲明是重要程度最低的,而用戶只有將該聲明標記為“重要”才可以替換網頁作者的聲明。同樣順序的聲明會根據特異性進行排序,然后再是其指定順序。HTML 可視化屬性會轉換成匹配的 CSS 聲明。它們被視為低優先級的網頁作者規則。
特異性
選擇器的特異性由 CSS2 規范定義如下:
- 如果聲明來自于“style”屬性,而不是帶有選擇器的規則,則記為 1,否則記為 0 (= a)
- 記為選擇器中 ID 屬性的個數 (= b)
- 記為選擇器中其他屬性和偽類的個數 (= c)
- 記為選擇器中元素名稱和偽元素的個數 (= d)
將四個數字按 a-b-c-d 這樣連接起來(位于大數進制的數字系統中),構成特異性。您使用的進制取決于上述類別中的最高計數。
例如,如果 a=14,您可以使用十六進制。如果 a=17,那么您需要使用十七進制;當然不太可能出現這種情況,除非是存在如下的選擇器:html body div div p …(在選擇器中出現了 17 個標記,這樣的可能性極低)。
一些示例:
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */ li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */ li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */ ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */ ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */ h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */ ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */ li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */ #x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */ style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
規則排序
找到匹配的規則之后,應根據級聯順序將其排序。Webkit 對于較小的列表會使用冒泡排序,而對較大的列表則使用歸并排序。對于以下規則,Webkit 通過替換“>”運算符來實現排序:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2) { int spec1 = r1.selector()->specificity(); int spec2 = r2.selector()->specificity(); return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; }
漸進式處理
Webkit 使用一個標記來表示是否所有的頂級樣式表(包括 @imports)均已加載完畢。如果在附加過程中尚未完全加載樣式,則使用占位符,并在文檔中進行標注,等樣式表加載完畢后再重新計算。
布局
呈現器在創建完成并添加到呈現樹時,并不包含位置和大小信息。計算這些值的過程稱為布局或重排。
HTML 采用基于流的布局模型,這意味著大多數情況下只要一次遍歷就能計算出幾何信息。處于流中靠后位置元素通常不會影響靠前位置元素的幾何特征,因此布局可以按從左至右、從上至下的順序遍歷文檔。但是也有例外情況,比如 HTML 表格的計算就需要不止一次的遍歷 (3.5)。
坐標系是相對于根框架而建立的,使用的是上坐標和左坐標。
布局是一個遞歸的過程。它從根呈現器(對應于 HTML 文檔的 <html>
元素)開始,然后遞歸遍歷部分或所有的框架層次結構,為每一個需要計算的呈現器計算幾何信息。
根呈現器的位置左邊是 0,0,其尺寸為視口(也就是瀏覽器窗口的可見區域)。所有的呈現器都有一個“laybout”或者“reflow”方法,每一個呈現器都會調用其需要進行布局的子代的 layout 方法。
Dirty 位系統
為避免對所有細小更改都進行整體布局,瀏覽器采用了一種“dirty 位”系統。如果某個呈現器發生了更改,或者將自身及其子代標注為“dirty”,則需要進行布局。
有兩種標記:“dirty”和“children are dirty”。“children are dirty”表示盡管呈現器自身沒有變化,但它至少有一個子代需要布局。
全局布局和增量布局
全局布局是指觸發了整個呈現樹范圍的布局,觸發原因可能包括:
- 影響所有呈現器的全局樣式更改,例如字體大小更改。
- 屏幕大小調整。
布局可以采用增量方式,也就是只對 dirty 呈現器進行布局(這樣可能存在需要進行額外布局的弊端)。
當呈現器為 dirty 時,會異步觸發增量布局。例如,當來自網絡的額外內容添加到 DOM 樹之后,新的呈現器附加到了呈現樹中。
圖:增量布局 – 只有 dirty 呈現器及其子代進行布局 (3.6)。
異步布局和同步布局
增量布局是異步執行的。Firefox 將增量布局的“reflow 命令”加入隊列,而調度程序會觸發這些命令的批量執行。Webkit 也有用于執行增量布局的計時器:對呈現樹進行遍歷,并對 dirty 呈現器進行布局。
請求樣式信息(例如“offsetHeight”)的腳本可同步觸發增量布局。
全局布局往往是同步觸發的。
有時,當初始布局完成之后,如果一些屬性(如滾動位置)發生變化,布局就會作為回調而觸發。
優化
如果布局是由“大小調整”或呈現器的位置(而非大小)改變而觸發的,那么可以從緩存中獲取呈現器的大小,而無需重新計算。
在某些情況下,只有一個子樹進行了修改,因此無需從根節點開始布局。這適用于在本地進行更改而不影響周圍元素的情況,例如在文本字段中插入文本(否則每次鍵盤輸入都將觸發從根節點開始的布局)。
布局處理
布局通常具有以下模式:
- 父呈現器確定自己的寬度。
-
父呈現器依次處理子呈現器,并且:
- 放置子呈現器(設置 x,y 坐標)。
- 如果有必要,調用子呈現器的布局(如果子呈現器是 dirty 的,或者這是全局布局,或出于其他某些原因),這會計算子呈現器的高度。
- 父呈現器根據子呈現器的累加高度以及邊距和補白的高度來設置自身高度,此值也可供父呈現器的父呈現器使用。
- 將其 dirty 位設置為 false。
Firefox 使用“state”對象 (nsHTMLReflowState) 作為布局的參數(稱為“reflow”),這其中包括了父呈現器的寬度。
Firefox 布局的輸出為“metrics”對象 (nsHTMLReflowMetrics),其包含計算得出的呈現器高度。
寬度計算
呈現器寬度是根據容器塊的寬度、呈現器樣式中的“width”屬性以及邊距和邊框計算得出的。
例如以下 div 的寬度:
<div style="width:30%"/>
將由 Webkit 計算如下(BenderBox 類,calcWidth 方法):
-
容器的寬度取容器的 availableWidth 和 0 中的較大值。availableWidth 在本例中相當于 contentWidth,計算公式如下:
clientWidth() - paddingLeft() - paddingRight()
clientWidth 和 clientHeight 表示一個對象的內部(除去邊框和滾動條)。
- 元素的寬度是“width”樣式屬性。它會根據容器寬度的百分比計算得出一個絕對值。
- 然后加上水平方向的邊框和補白。
現在計算得出的是“preferred width”。然后需要計算最小寬度和最大寬度。
如果首選寬度大于最大寬度,那么應使用最大寬度。如果首選寬度小于最小寬度(最小的不可破開單位),那么應使用最小寬度。這些值會緩存起來,以用于需要布局而寬度不變的情況。
換行
如果呈現器在布局過程中需要換行,會立即停止布局,并告知其父代需要換行。父代會創建額外的呈現器,并對其調用布局。
繪制
在繪制階段,系統會遍歷呈現樹,并調用呈現器的“paint”方法,將呈現器的內容顯示在屏幕上。繪制工作是使用用戶界面基礎組件完成的。
全局繪制和增量繪制
和布局一樣,繪制也分為全局(繪制整個呈現樹)和增量兩種。在增量繪制中,部分呈現器發生了更改,但是不會影響整個樹。更改后的呈現器將其在屏幕上對應的 矩形區域設為無效,這導致 OS 將其視為一塊“dirty 區域”,并生成“paint”事件。OS 會很巧妙地將多個區域合并成一個。在 Chrome 瀏覽器中,情況要更復雜一些,因為 Chrome 瀏覽器的呈現器不在主進程上。Chrome 瀏覽器會在某種程度上模擬 OS 的行為。展示層會偵聽這些事件,并將消息委托給呈現根節點。然后遍歷呈現樹,直到找到相關的呈現器,該呈現器會重新繪制自己(通常也包括其子代)。
繪制順序
CSS2 規范定義了繪制流程的順序。繪制的順序其實就是元素進入堆棧樣式上下文的順序。這些堆棧會從后往前繪制,因此這樣的順序會影響繪制。塊呈現器的堆棧順序如下:
- 背景顏色
- 背景圖片
- 邊框
- 子代
- 輪廓
Firefox 顯示列表
Firefox 遍歷整個呈現樹,為繪制的矩形建立一個顯示列表。列表中按照正確的繪制順序(先是呈現器的背景,然后是邊框等等)包含了與矩形相關的呈現器。這樣等到重新 繪制的時候,只需遍歷一次呈現樹,而不用多次遍歷(繪制所有背景,然后繪制所有圖片,再繪制所有邊框等等)。Firefox 對此過程進行了優化,也就是不添加隱藏的元素,例如被不透明元素完全遮擋住的元素。
Webkit 矩形存儲
在重新繪制之前,Webkit 會將原來的矩形另存為一張位圖,然后只繪制新舊矩形之間的差異部分。
動態變化
在發生變化時,瀏覽器會盡可能做出最小的響應。因此,元素的顏色改變后,只會對該元素進行重繪。元素的位置改變后,只會對該元素及其子元素(可能還有同級 元素)進行布局和重繪。添加 DOM 節點后,會對該節點進行布局和重繪。一些重大變化(例如增大“html”元素的字體)會導致緩存無效,使得整個呈現樹都會進行重新布局和繪制。
呈現引擎的線程
呈現引擎采用了單線程。幾乎所有操作(除了網絡操作)都是在單線程中進行的。在 Firefox 和 Safari 中,該線程就是瀏覽器的主線程。而在 Chrome 瀏覽器中,該線程是標簽進程的主線程。
網絡操作可由多個并行線程執行。并行連接數是有限的(通常為 2 至 6 個,以 Firefox 3 為例是 6 個)。
事件循環
瀏覽器的主線程是事件循環。它是一個無限循環,永遠處于接受處理狀態,并等待事件(如布局和繪制事件)發生,并進行處理。這是 Firefox 中關于主事件循環的代碼:
while (!mExiting) NS_ProcessNextEvent(thread);
CSS2 可視化模型
畫布
根據 CSS2 規范,“畫布”這一術語是指“用來呈現格式化結構的空間”,也就是供瀏覽器繪制內容的區域。畫布的空間尺寸大小是無限的,但是瀏覽器會根據視口的尺寸選擇一個初始寬度。
根據 www.w3.org/TR/CSS2/zindex.html,畫布如果包含在其他畫布內,就是透明的;否則會由瀏覽器指定一種顏色。
CSS 框模型
CSS 框模型描述的是針對文檔樹中的元素而生成,并根據可視化格式模型進行布局的矩形框。
每個框都有一個內容區域(例如文本、圖片等),還有可選的周圍補白、邊框和邊距區域。
圖:CSS2 框模型
每一個節點都會生成 0..n 個這樣的框。
所有元素都有一個“display”屬性,決定了它們所對應生成的框類型。示例:
block - generates a block box. inline - generates one or more inline boxes. none - no box is generated.
默認值是 inline,但是瀏覽器樣式表設置了其他默認值。例如,“div”元素的 display 屬性默認值是 block。
您可以在這里找到默認樣式表示例:www.w3.org/TR/CSS2/sample.html
定位方案
有三種定位方案:
- 普通:根據對象在文檔中的位置進行定位,也就是說對象在呈現樹中的位置和它在 DOM 樹中的位置相似,并根據其框類型和尺寸進行布局。
- 浮動:對象先按照普通流進行布局,然后盡可能地向左或向右移動。
- 絕對:對象在呈現樹中的位置和它在 DOM 樹中的位置不同。
定位方案是由“position”屬性和“loat”屬性設置的。
- 如果值是 static 和 relative,就是普通流
- 如果值是 absolute 和 fixed,就是絕對定位
static 定位無需定義位置,而是使用默認定位。對于其他方案,網頁作者需要指定位置:top、bottom、left、right。框的布局方式是由以下因素決定的:
- 框類型
- 框尺寸
- 定位方案
- 外部信息,例如圖片大小和屏幕大小
框類型
block 框:形成一個 block,在瀏覽器窗口中擁有其自己的矩形區域。
圖:block 框
inline 框:沒有自己的 block,但是位于容器 block 內。
圖:inline 框
block 采用的是一個接一個的垂直格式,而 inline 采用的是水平格式。
圖:block 和 inline 格式
inline 框放置在行中或“行框”中。這些行至少和最高的框一樣高,還可以更高,當框根據“底線”對齊時,這意味著元素的底部需要根據其他框中非底部的位置對齊。如果容器的寬度不夠,inline 元素就會分為多行放置。在段落中經常發生這種情況。
圖:行
定位
相對
相對定位:先按照普通方式定位,然后根據所需偏移量進行移動。
圖:相對定位
浮動
浮動框會移動到行的左邊或右邊。有趣的特征在于,其他框會浮動在它的周圍。下面這段 HTML 代碼:
<p> <img style="float:right" src="images/image.gif" width="100" height="100"> Lorem ipsum dolor sit amet, consectetuer... </p>
顯示效果如下:
圖:浮動
絕對定位和固定定位
這種布局是準確定義的,與普通流無關。元素不參與普通流。尺寸是相對于容器而言的。在固定定位中,容器就是可視區域。
圖:固定定位
請注意,即使在文檔滾動時,固定框也不會移動。
分層展示
這是由 z-index CSS 屬性指定的。它代表了框的第三個維度,也就是沿“z 軸”方向的位置。
這些框分散到多個堆棧(稱為堆棧上下文)中。在每一個堆棧中,會首先繪制后面的元素,然后在頂部繪制前面的元素,以便更靠近用戶。如果出現重疊,新繪制的元素就會覆蓋之前的元素。
堆棧是按照 z-index 屬性進行排序的。具有“z-index”屬性的框形成了本地堆棧。視口具有外部堆棧。
示例:
<style type="text/css"> div { position: absolute; left: 2in; top: 2in; } </style> <p> <div style="z-index: 3;background-color:red; width: 1in; height: 1in; "> </div> <div style="z-index: 1;background-color:green;width: 2in; height: 2in;"> </div> </p>
結果如下:
圖:固定定位
雖然紅色 div 在標記中的位置比綠色 div 靠前(按理應該在常規流程中優先繪制),但是 z-index 屬性的優先級更高,因此它移動到了根框所保持的堆棧中更靠前的位置。
參考資料
-
瀏覽器架構
- Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
- Gupta, Vineet. How Browsers Work – Part 1 – Architecture
-
解析
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools(即“Dragon book”), Addison-Wesley, 1986
- Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5.
-
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers(Google 技術訪談視頻)
- L. David Baron, Mozilla’s Layout Engine
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- Chris Waterson, Gecko Overview
- Alexander Larsson, The life of an HTML HTTP request
-
Webkit
- David Hyatt, Implementing CSS(第一部分)
- David Hyatt, An Overview of WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, The FOUC Problem
- W3C 規范
- 瀏覽器構建說明
原文:http://taligarsiel.com/Projects/howbrowserswork1.htm
文章列表