序言這是一篇全面介紹 WebKit 和 Gecko 內部操作的入門文章,是以色列開發人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了所有公開發佈的關於瀏覽器內部機制的數據(請參見資源),並花了很多時間來研讀網路瀏覽器的源代碼。她寫道:在 IE 占據 90% 市場份額的年代,我們除了把瀏覽器當...
序言
這是一篇全面介紹 WebKit 和 Gecko 內部操作的入門文章,是以色列開發人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了所有公開發佈的關於瀏覽器內部機制的數據(請參見資源),並花了很多時間來研讀網路瀏覽器的源代碼。她寫道:
在 IE 占據 90% 市場份額的年代,我們除了把瀏覽器當成一個“黑箱”,什麼也做不了。但是現在,開放源代碼的瀏覽器擁有了過半的市場份額,因此,是時候來揭開神秘的面紗,一探網路瀏覽器的內幕了。呃,裡面只有數以百萬行計的 C++ 代碼...塔利在她的網站上公佈了自己的研究成果,但是我們覺得它值得讓更多的人來瞭解,所以我們在此重新整理並公佈。
作為一名網路開發人員,學習瀏覽器的內部工作原理將有助於您作出更明智的決策,並理解那些最佳開發實踐的個中緣由。儘管這是一篇相當長的文檔,但是我們建議您花些時間來仔細閱讀;讀完之後,您肯定會覺得所費不虛。 保羅·愛麗詩 (Paul Irish),Chrome 瀏覽器開發人員事務部
簡介
網路瀏覽器很可能是使用最廣的軟體。在這篇入門文章中,我將會介紹它們的幕後工作原理。我們會瞭解到,從您在地址欄輸入 google.com
直到您在瀏覽器屏幕上看到 Google 首頁的整個過程中都發生了些什麼。
目錄
我們要討論的瀏覽器
目前使用的主流瀏覽器有五個: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):
- 用戶界面 - 包括地址欄、前進/後退按鈕、書簽菜單等。除了瀏覽器主視窗顯示的您請求的頁面外,其他顯示的各個部分都屬於用戶界面。
- 瀏覽器引擎 - 在用戶界面和呈現引擎之間傳送指令。
- 呈現引擎 - 負責顯示請求的內容。如果請求的內容是 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.6)從圖 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,同時模式轉變為“in 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);
b