瀏覽器的工作原理:新式網路瀏覽器幕後揭秘

来源:http://www.cnblogs.com/double405/archive/2016/01/13/5127494.html
-Advertisement-
Play Games

序言這是一篇全面介紹 WebKit 和 Gecko 內部操作的入門文章,是以色列開發人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了所有公開發佈的關於瀏覽器內部機制的數據(請參見資源),並花了很多時間來研讀網路瀏覽器的源代碼。她寫道:在 IE 占據 90% 市場份額的年代,我們除了把瀏覽器當...


序言

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

在 IE 占據 90% 市場份額的年代,我們除了把瀏覽器當成一個“黑箱”,什麼也做不了。但是現在,開放源代碼的瀏覽器擁有了過半的市場份額,因此,是時候來揭開神秘的面紗,一探網路瀏覽器的內幕了。呃,裡面只有數以百萬行計的 C++ 代碼...
塔利在她的網站上公佈了自己的研究成果,但是我們覺得它值得讓更多的人來瞭解,所以我們在此重新整理並公佈。

作為一名網路開發人員,學習瀏覽器的內部工作原理將有助於您作出更明智的決策,並理解那些最佳開發實踐的個中緣由。儘管這是一篇相當長的文檔,但是我們建議您花些時間來仔細閱讀;讀完之後,您肯定會覺得所費不虛。 保羅·愛麗詩 (Paul Irish),Chrome 瀏覽器開發人員事務部


簡介

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

目錄

  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%。由此可見,如今開放源代碼瀏覽器在瀏覽器市場中占據了非常堅實的部分。

瀏覽器的主要功能

瀏覽器的主要功能就是向伺服器發出請求,在瀏覽器視窗中展示您選擇的網路資源。這裡所說的資源一般是指 HTML 文檔,也可以是 PDF、圖片或其他的類型。資源的位置由用戶使用 URI(統一資源標示符)指定。

瀏覽器解釋並顯示 HTML 文件的方式是在 HTML 和 CSS 規範中指定的。這些規範由網路標準化組織 W3C(萬維網聯盟)進行維護。
多年以來,各瀏覽器都沒有完全遵從這些規範,同時還在開發自己獨有的擴展程式,這給網路開發人員帶來了嚴重的相容性問題。如今,大多數的瀏覽器都是或多或少地遵從規範。

瀏覽器的用戶界面有很多彼此相同的元素,其中包括:

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

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

瀏覽器的高層結構

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

  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> 標記也會進行同樣的處理。目前 htmlbody 標記均已發出。現在我們回到“數據狀態”。接收到 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);
            b

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文總結了20種ios濾鏡都是基於GPUImage的,有3種濾鏡是GPUImage庫中包含的,還有17種是Instagram中的經典濾鏡,集成在一個項目中。使用GPUImage可以非常容易創建我們自己的濾鏡效果總會有你想要的效果吧。在文章下麵附源碼下載 相信你也在使用濾鏡吧,今天就讓你見識一下...
  • 雜談在進行android進行開發時,我們的數據一般通過介面來獲收,這裡指的介面泛指web api,webservice,wcf,web應用程式等;它們做為服務端與資料庫進行直接通訊,而APP這塊通過向這些介面發Http請求來獲得數據,這樣的好處大叔認為,可以有效的降低軟體的開發難度,所以數據交互都被...
  • JavaScript數組常用操作、總結
  • jQuery
  • 話說JSON數據平常用的確實挺多的,但是基本上只知道怎麼用,對其一些細節並沒有整理過,今兒趁著下午有點空,坐下來,學習整理下,並分享出來。 對於JSON,首先它只是一種數據格式,並非一種語言,雖然和javascript長的比較像,但並不從屬於javascript。如果你使用過其他編程...
  • 需求:在富文本編輯器中插入圖片,圖片來自用戶可以自己上傳的圖片庫。本來可以用比較噁心的方式,也就是直接用tinyMCE自帶的插入圖片插件來實現。噁心是因為這個圖片插件需要用戶填入圖片的url。想來想去,雖然是內部管理平臺產品1期,但作為一個21世紀的程式猿做這樣的事兒太low了,而且也怕被同事圍毆,...
  • 好久沒更新博客了...自從有了mac之後世界變得簡單了。。。日常麽,除了研究代碼,看別人的代碼,寫自己的代碼。就那樣。。。。吐槽點:window配個nodejs的環境花了九頭牛兩隻老虎的力氣,mac上只要安裝,就可以啪啪啪啪了。。。。瞬間無力吐槽。。。今天突然想到了偶的徒弟上次說貌似不會用retur...
  • 好久沒登錄博客園了,今天來一發分享。 最近項目里有個需求,上傳文件(好吧,這種需求很常見,這也不是第一次遇到了)。當時第一想法就是直接用form表單提交(原諒我以前就是這麼乾的),不過表單里不僅有文件還有別的信息需要交互,跟後端商量後決定文件單獨上傳,獲取到伺服器端返回的文件地址在和表單一起提...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...