瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕

序言

這是一篇全面介紹 WebKit 和 Gecko 內部操做的入門文章,是以色列開發人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了全部公開發布的關於瀏覽器內部機制的數據(請參見資源),並花了不少時間來研讀網絡瀏覽器的源代碼。她寫道:css

在 IE 佔據 90% 市場份額的年代,咱們除了把瀏覽器當成一個「黑箱」,什麼也作不了。可是如今,開放源代碼的瀏覽器擁有了 過半的市場份額,所以,是時候來揭開神祕的面紗,一探網絡瀏覽器的內幕了。呃,裏面只有數以百萬行計的 C++ 代碼...

塔利在她的網站上公佈了本身的研究成果,可是咱們以爲它值得讓更多的人來了解,因此咱們在此從新整理並公佈。html

做爲一名網絡開發人員,學習瀏覽器的內部工做原理將有助於您做出更明智的決策,並理解那些最佳開發實踐的箇中原因。儘管這是一篇至關長的文檔,可是咱們建議您花些時間來仔細閱讀;讀完以後,您確定會以爲所費不虛。保羅·愛麗詩 (Paul Irish),Chrome 瀏覽器開發人員事務部html5


簡介

網絡瀏覽器極可能是使用最廣的軟件。在這篇入門文章中,我將會介紹它們的幕後工做原理。咱們會了解到,從您在地址欄輸入 google.com 直到您在瀏覽器屏幕上看到 Google 首頁的整個過程當中都發生了些什麼。node

目錄

  1. 簡介
    1. 咱們要討論的瀏覽器
    2. 瀏覽器的主要功能
    3. 瀏覽器的高層結構
  2. 呈現引擎
    1. 呈現引擎
    2. 主流程
    3. 主流程示例
  3. 解析和 DOM 樹構建
    1. 解析 - 綜述
      1. 語法
      2. 解析器和詞法分析器的組合
      3. 翻譯
      4. 解析示例
      5. 詞彙和語法的正式定義
      6. 解析器類型
      7. 自動生成解析器
    2. HTML 解析器
      1. HTML 語法定義
      2. 非與上下文無關的語法
      3. HTML DTD
      4. DOM
      5. 解析算法
      6. 標記化算法
      7. 樹構建算法
      8. 解析結束後的操做
      9. 瀏覽器的容錯機制
    3. CSS 解析
      1. WebKit CSS 解析器
    4. 處理腳本和樣式表的順序
      1. 腳本
      2. 預解析
      3. 樣式表
  4. 呈現樹構建
    1. 呈現樹和 DOM 樹的關係
    2. 構建呈現樹的流程
    3. 樣式計算
      1. 共享樣式數據
      2. Firefox 規則樹
        1. 結構劃分
        2. 使用規則樹計算樣式上下文
      3. 對規則進行處理以簡化匹配
      4. 以正確的層疊順序應用規則
        1. 樣式表層疊順序
        2. 特異性
        3. 規則排序
    4. 漸進式處理
  5. 佈局
    1. Dirty 位系統
    2. 全局佈局和增量佈局
    3. 異步佈局和同步佈局
    4. 優化
    5. 佈局處理
    6. 寬度計算
    7. 換行
  6. 繪製
    1. 全局繪製和增量繪製
    2. 繪製順序
    3. Firefox 顯示列表
    4. WebKit 矩形存儲
  7. 動態變化
  8. 呈現引擎的線程
    1. 事件循環
  9. CSS2 可視化模型
    1. 畫布
    2. CSS 框模型
    3. 定位方案
    4. 框類型
    5. 定位
      1. 相對定位
      2. 浮動定位
      3. 絕對定位和固定定位
    6. 分層展現
  10. 資源

咱們要討論的瀏覽器

目前使用的主流瀏覽器有五個:Internet Explorer、Firefox、Safari、Chrome 瀏覽器和 Opera。本文中以開放源代碼瀏覽器爲例,即 Firefox、Chrome 瀏覽器和 Safari(部分開源)。根據 StatCounter 瀏覽器統計數據,目前(2011 年 8 月)Firefox、Safari 和 Chrome 瀏覽器的總市場佔有率將近 60%。因而可知,現在開放源代碼瀏覽器在瀏覽器市場中佔據了很是堅實的部分。web

瀏覽器的主要功能

瀏覽器的主要功能就是向服務器發出請求,在瀏覽器窗口中展現您選擇的網絡資源。這裏所說的資源通常是指 HTML 文檔,也能夠是 PDF、圖片或其餘的類型。資源的位置由用戶使用 URI(統一資源標示符)指定。正則表達式

瀏覽器解釋並顯示 HTML 文件的方式是在 HTML 和 CSS 規範中指定的。這些規範由網絡標準化組織 W3C(萬維網聯盟)進行維護。
多年以來,各瀏覽器都沒有徹底聽從這些規範,同時還在開發本身獨有的擴展程序,這給網絡開發人員帶來了嚴重的兼容性問題。現在,大多數的瀏覽器都是或多或少地聽從規範。算法

瀏覽器的用戶界面有不少彼此相同的元素,其中包括:數據庫

  • 用來輸入 URI 的地址欄
  • 前進和後退按鈕
  • 書籤設置選項
  • 用於刷新和中止加載當前文檔的刷新和中止按鈕
  • 用於返回主頁的主頁按鈕

奇怪的是,瀏覽器的用戶界面並無任何正式的規範,這是多年來的最佳實踐天然發展以及彼此之間相互模仿的結果。HTML5 也沒有定義瀏覽器必須具備的用戶界面元素,但列出了一些通用的元素,例如地址欄、狀態欄和工具欄等。固然,各瀏覽器也能夠有本身獨特的功能,好比 Firefox 的下載管理器。express

瀏覽器的高層結構

瀏覽器的主要組件爲 (1.1):canvas

  1. 用戶界面 - 包括地址欄、前進/後退按鈕、書籤菜單等。除了瀏覽器主窗口顯示的您請求的頁面外,其餘顯示的各個部分都屬於用戶界面。
  2. 瀏覽器引擎 - 在用戶界面和呈現引擎之間傳送指令。
  3. 呈現引擎 - 負責顯示請求的內容。若是請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。
  4. 網絡 - 用於網絡調用,好比 HTTP 請求。其接口與平臺無關,併爲全部平臺提供底層實現。
  5. 用戶界面後端 - 用於繪製基本的窗口小部件,好比組合框和窗口。其公開了與平臺無關的通用接口,而在底層使用操做系統的用戶界面方法。
  6. JavaScript 解釋器。用於解析和執行 JavaScript 代碼。
  7. 數據存儲。這是持久層。瀏覽器須要在硬盤上保存各類數據,例如 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.6)

從圖 3 和圖 4 能夠看出,雖然 WebKit 和 Gecko 使用的術語略有不一樣,但總體流程是基本相同的。

Gecko 將視覺格式化元素組成的樹稱爲「框架樹」。每一個元素都是一個框架。WebKit 使用的術語是「呈現樹」,它由「呈現對象」組成。對於元素的放置,WebKit 使用的術語是「佈局」,而 Gecko 稱之爲「重排」。對於鏈接 DOM 節點和可視化信息從而建立呈現樹的過程,WebKit 使用的術語是「附加」。有一個細微的非語義差異,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱爲「內容槽」的層,用於生成 DOM 元素。咱們會逐一論述流程中的每一部分:

 

解析 - 綜述

解析是呈現引擎中很是重要的一個環節,所以咱們要更深刻地講解。首先,來介紹一下解析。

解析文檔是指將文檔轉化成爲有意義的結構,也就是可以讓代碼理解和使用的結構。解析獲得的結果一般是表明了文檔結構的節點樹,它稱做解析樹或者語法樹。

示例 - 解析 2 + 3 - 1 這個表達式,會返回下面的樹:

圖:數學表達式樹節點

語法

解析是以文檔所遵循的語法規則(編寫文檔所用的語言或格式)爲基礎的。全部能夠解析的格式都必須對應肯定的語法(由詞彙和語法規則構成)。這稱爲與上下文無關的語法。人類語言並不屬於這樣的語言,所以沒法用常規的解析技術進行解析。

解析器和詞法分析器的組合

解析的過程能夠分紅兩個子過程:詞法分析和語法分析。

詞法分析是將輸入內容分割成大量標記的過程。標記是語言中的詞彙,即構成內容的單位。在人類語言中,它至關於語言字典中的單詞。

語法分析是應用語言的語法規則的過程。

解析器一般將解析工做分給如下兩個組件來處理:詞法分析器(有時也稱爲標記生成器),負責將輸入內容分解成一個個有效標記;而解析器負責根據語言的語法規則分析文檔的結構,從而構建解析樹。詞法分析器知道如何將無關的字符(好比空格和換行符)分離出來。

圖:從源文檔到解析樹

解析是一個迭代的過程。一般,解析器會向詞法分析器請求一個新標記,並嘗試將其與某條語法規則進行匹配。若是發現了匹配規則,解析器會將一個對應於該標記的節點添加到解析樹中,而後繼續請求下一個標記。

若是沒有規則能夠匹配,解析器就會將標記存儲到內部,並繼續請求標記,直至找到可與全部內部存儲的標記匹配的規則。若是找不到任何匹配規則,解析器就會引起一個異常。這意味着文檔無效,包含語法錯誤。

翻譯

不少時候,解析樹還不是最終產品。解析一般是在翻譯過程當中使用的,而翻譯是指將輸入文檔轉換成另外一種格式。編譯就是這樣一個例子。編譯器可將源代碼編譯成機器代碼,具體過程是首先將源代碼解析成解析樹,而後將解析樹翻譯成機器代碼文檔。

圖:編譯流程

解析示例

在圖 5 中,咱們經過一個數學表達式創建瞭解析樹。如今,讓咱們試着定義一個簡單的數學語言,用來演示解析的過程。

 

詞彙:咱們用的語言可包含整數、加號和減號。

語法:

  1. 構成語言的語法單位是表達式、項和運算符。
  2. 咱們用的語言能夠包含任意數量的表達式。
  3. 表達式的定義是:一個「項」接一個「運算符」,而後再接一個「項」。
  4. 運算符是加號或減號。
  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 沒法用常規的自上而下或自下而上的解析器進行解析。

緣由在於:

  1. 語言的寬容本質。
  2. 瀏覽器從來對一些常見的無效 HTML 用法採起包容態度。
  3. 解析過程須要不斷地反覆。源內容在解析過程當中一般不會改變,可是在 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,同時模式轉變爲「in body」

如今,接收由「Hello world」字符串生成的一系列字符標記。接收第一個字符時會建立並插入「Text」節點,而其餘字符也將附加到該節點。

接收 body 結束標記會觸發「after body」模式。如今咱們將接收 HTML 結束標記,而後進入「after after body」模式。接收到文件結束標記後,解析過程就此結束。

圖:示例 HTML 的樹構建

解析結束後的操做

在此階段,瀏覽器會將文檔標註爲交互狀態,並開始解析那些處於「deferred」模式的腳本,也就是那些應在文檔解析完成後才執行的腳本。而後,文檔狀態將設置爲「完成」,一個「加載」事件將隨之觸發。

您能夠在 HTML5 規範中查看標記化和樹構建的完整算法

瀏覽器的容錯機制

您在瀏覽 HTML 網頁時歷來不會看到「語法無效」的錯誤。這是由於瀏覽器會糾正任何無效內容,而後繼續工做。

如下面的 HTML 代碼爲例:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

在這裏,我已經違反了不少語法規則(「mytag」不是標準的標記,「p」和「div」元素之間的嵌套有誤等等),可是瀏覽器仍然會正確地顯示這些內容,而且毫無怨言。由於有大量的解析器代碼會糾正 HTML 網頁做者的錯誤。

不一樣瀏覽器的錯誤處理機制至關一致,但使人稱奇的是,這種機制並非 HTML 當前規範的一部分。和書籤管理以及前進/後退按鈕同樣,它也是瀏覽器在多年發展中的產物。不少網站都廣泛存在着一些已知的無效 HTML 結構,每一種瀏覽器都會嘗試經過和其餘瀏覽器同樣的方式來修復這些無效結構。

HTML5 規範定義了一部分這樣的要求。WebKit 在 HTML 解析器類的開頭註釋中對此作了很好的歸納。

解析器對標記化輸入內容進行解析,以構建文檔樹。若是文檔的格式正確,就直接進行解析。

遺憾的是,咱們不得不處理不少格式錯誤的 HTML 文檔,因此解析器必須具有必定的容錯性。

咱們至少要可以處理如下錯誤狀況:

  1. 明顯不能在某些外部標記中添加的元素。在此狀況下,咱們應該關閉全部標記,直到出現禁止添加的元素,而後再加入該元素。
  2. 咱們不能直接添加的元素。這極可能是網頁做者忘記添加了其中的一些標記(或者其中的標記是可選的)。這些標籤可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
  3. 向 inline 元素內添加 block 元素。關閉全部 inline 元素,直到出現下一個較高級的 block 元素。
  4. 若是這樣仍然無效,可關閉全部元素,直到能夠添加元素爲止,或者忽略該標記。

讓咱們看一些 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」文件夾下)。

樣式計算存在如下難點:

  1. 樣式數據是一個超大的結構,存儲了無數的樣式屬性,這可能形成內存問題。
  2. 若是不進行優化,爲每個元素查找匹配的規則會形成性能問題。要爲每個元素遍歷整個規則列表來尋找匹配規則,這是一項浩大的工程。選擇器會具備很複雜的結構,這就會致使某個匹配過程一開始看起來極可能是正確的,但最終發現實際上是徒勞的,必須嘗試其餘匹配路徑。

    例以下面這個組合選擇器:

    div div div div{
      ...
    }
    這意味着規則適用於做爲 3 個 div 元素的子代的 <div>。若是您要檢查規則是否適用於某個指定的 <div> 元素,應選擇樹上的一條向上路徑進行檢查。您可能須要向上遍歷節點樹,結果發現只有兩個 div,並且規則並不適用。而後,您必須嘗試樹中的其餘路徑。
  3. 應用規則涉及到至關複雜的層疊規則(用於定義這些規則的層次)。

讓咱們來看看瀏覽器是如何處理這些問題的:

共享樣式數據

WebKit 節點會引用樣式對象 (RenderStyle)。這些對象在某些狀況下能夠由不一樣節點共享。這些節點是同級關係,而且:

  1. 這些元素必須處於相同的鼠標狀態(例如,不容許其中一個是「:hover」狀態,而另外一個不是)
  2. 任何元素都沒有 ID
  3. 標記名稱應匹配
  4. 類屬性應匹配
  5. 映射屬性的集合必須是徹底相同的
  6. 連接狀態必須匹配
  7. 焦點狀態必須匹配
  8. 任何元素都不該受屬性選擇器的影響,這裏所說的「影響」是指在選擇器中的任何位置有任何使用了屬性選擇器的選擇器匹配
  9. 元素中不能有任何 inline 樣式屬性
  10. 不能使用任何同級選擇器。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>

還有以下規則:

 
  1. div {margin:5px;color:black}
  2. .err {color:red}
  3. .big {margin-top:3px}
  4. div span {margin-bottom:4px}
  5. #div1 {color:blue}
  6. #div2 {color:green}

爲了簡便起見,咱們只須要填充兩個結構:color 結構和 margin 結構。color 結構只包含一個成員(即「color」),而 margin 結構包含四條邊。
造成的規則樹以下圖所示(節點的標記方式爲「節點名 : 指向的規則序號」):

圖:規則樹


上下文樹以下圖所示(節點名 : 指向的規則節點):

圖:上下文樹

假設咱們解析 HTML 時遇到了第二個 <div> 標記,咱們須要爲此節點建立樣式上下文,並填充其樣式結構。
通過規則匹配,咱們發現該 <div> 的匹配規則是第 一、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 規範,層疊的順序爲(優先級從低到高):

  1. 瀏覽器聲明
  2. 用戶普通聲明
  3. 做者普通聲明
  4. 做者重要聲明
  5. 用戶重要聲明

 

瀏覽器聲明是重要程度最低的,而用戶只有將該聲明標記爲「重要」才能夠替換網頁做者的聲明。一樣順序的聲明會根據特異性進行排序,而後再是其指定順序。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,其尺寸爲視口(也就是瀏覽器窗口的可見區域)。

全部的呈現器都有一個「layout」或者「reflow」方法,每個呈現器都會調用其須要進行佈局的子代的 layout 方法。

Dirty 位系統

爲避免對全部細小更改都進行總體佈局,瀏覽器採用了一種「dirty 位」系統。若是某個呈現器發生了更改,或者將自身及其子代標註爲「dirty」,則須要進行佈局。

有兩種標記:「dirty」和「children are dirty」。「children are dirty」表示儘管呈現器自身沒有變化,但它至少有一個子代須要佈局。

全局佈局和增量佈局

全局佈局是指觸發了整個呈現樹範圍的佈局,觸發緣由可能包括:

  1. 影響全部呈現器的全局樣式更改,例如字體大小更改。
  2. 屏幕大小調整。

 

佈局能夠採用增量方式,也就是隻對 dirty 呈現器進行佈局(這樣可能存在須要進行額外佈局的弊端)。
當呈現器爲 dirty 時,會異步觸發增量佈局。例如,當來自網絡的額外內容添加到 DOM 樹以後,新的呈現器附加到了呈現樹中。

圖:增量佈局 - 只有 dirty 呈現器及其子代進行佈局 (3.6)。

異步佈局和同步佈局

增量佈局是異步執行的。Firefox 將增量佈局的「reflow 命令」加入隊列,而調度程序會觸發這些命令的批量執行。WebKit 也有用於執行增量佈局的計時器:對呈現樹進行遍歷,並對 dirty 呈現器進行佈局。
請求樣式信息(例如「offsetHeight」)的腳本可同步觸發增量佈局。
全局佈局每每是同步觸發的。
有時,當初始佈局完成以後,若是一些屬性(如滾動位置)發生變化,佈局就會做爲回調而觸發。

優化

若是佈局是由「大小調整」或呈現器的位置(而非大小)改變而觸發的,那麼能夠從緩存中獲取呈現器的大小,而無需從新計算。
在某些狀況下,只有一個子樹進行了修改,所以無需從根節點開始佈局。這適用於在本地進行更改而不影響周圍元素的狀況,例如在文本字段中插入文本(不然每次鍵盤輸入都將觸發從根節點開始的佈局)。

 

佈局處理

佈局一般具備如下模式:

  1. 父呈現器肯定本身的寬度。
  2. 父呈現器依次處理子呈現器,而且:
    1. 放置子呈現器(設置 x,y 座標)。
    2. 若是有必要,調用子呈現器的佈局(若是子呈現器是 dirty 的,或者這是全局佈局,或出於其餘某些緣由),這會計算子呈現器的高度。
  3. 父呈現器根據子呈現器的累加高度以及邊距和補白的高度來設置自身高度,此值也可供父呈現器的父呈現器使用。
  4. 將其 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 規範定義了繪製流程的順序。繪製的順序其實就是元素進入堆棧樣式上下文的順序。這些堆棧會從後往前繪製,所以這樣的順序會影響繪製。塊呈現器的堆棧順序以下:

  1. 背景顏色
  2. 背景圖片
  3. 邊框
  4. 子代
  5. 輪廓

 

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

 

定位方案

有三種定位方案:

  1. 普通:根據對象在文檔中的位置進行定位,也就是說對象在呈現樹中的位置和它在 DOM 樹中的位置類似,並根據其框類型和尺寸進行佈局。
  2. 浮動:對象先按照普通流進行佈局,而後儘量地向左或向右移動。
  3. 絕對:對象在呈現樹中的位置和它在 DOM 樹中的位置不一樣。

 

定位方案是由「position」屬性和「float」屬性設置的。

  • 若是值是 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 屬性的優先級更高,所以它移動到了根框所保持的堆棧中更靠前的位置。

參考資料

  1. 瀏覽器架構
    1. Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
    2. Gupta, Vineet. How Browsers Work - Part 1 - Architecture
  2. 解析
    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools(即「Dragon book」), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5.
  3. Firefox
    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers(Google 技術訪談視頻)
    3. L. David Baron, Mozilla's Layout Engine
    4. L. David Baron, Mozilla Style System Documentation
    5. Chris Waterson, Notes on HTML Reflow
    6. Chris Waterson, Gecko Overview
    7. Alexander Larsson, The life of an HTML HTTP request
  4. WebKit
    1. David Hyatt, Implementing CSS(第一部分)
    2. David Hyatt, An Overview of WebCore
    3. David Hyatt, WebCore Rendering
    4. David Hyatt, The FOUC Problem
  5. W3C 規範
    1. HTML 4.01 規範
    2. W3C HTML5 規範
    3. 層疊樣式表第 2 級第 1 次修改 (CSS 2.1) 規範
  6. 瀏覽器構建說明
    1. Firefox. https://developer.mozilla.org/en/Build_Documentation
    2. WebKit. http://webkit.org/building/build.html

塔利·加希爾是以色列的一名開發人員。她在 2000 年開始從事網絡開發工做,逐漸熟悉了 Netscape 的「邪惡」層模型。就像理查德·費曼 (Richard Feynmann) 同樣,她極度熱衷於探究事物的原理,所以開始深刻了解瀏覽器的內部原理,並記錄研究成果。塔利還發表過一篇關於客戶端性能的簡短指南

翻譯狀況

此網頁已兩次翻譯爲日文!瀏覽器的工做原理:現代網絡瀏覽器幕後揭祕 (ja),譯者:@_kosei_;以及ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側,譯者:@ikeike443 和 @kiyoto01。感謝你們!