現代網路瀏覽器的幕後故事
前言
這份關於 WebKit 和 Gecko 內部運作的完整入門指南,是以色列開發人員 Tali Garsiel 進行大量研究的成果。幾年來,她查看了所有關於瀏覽器內部的公開資料,並花費大量時間閱讀網路瀏覽器原始碼。她寫道:
身為網頁程式開發人員,瞭解瀏覽器作業的內部機制,有助於做出更明智的決策,並瞭解開發最佳做法背後的理由。雖然這份文件相當冗長,但建議您花點時間深入瞭解。絕對值得您花時間進行這項作業。
Paul Irish,Chrome 開發人員關係
簡介
網路瀏覽器是最廣泛使用的軟體。在這份入門指南中,我會說明這些技術的運作原理。我們將觀察在網址列輸入 google.com 後,瀏覽器畫面顯示 Google 網頁前會發生什麼情況。
我們將介紹的瀏覽器
目前電腦上使用的五大瀏覽器包括 Chrome、Internet Explorer、Firefox、Safari 和 Opera。在行動裝置上,主要的瀏覽器包括 Android 瀏覽器、iPhone、Opera Mini 和 Opera Mobile、UC 瀏覽器、Nokia S40/S60 瀏覽器和 Chrome,除了 Opera 瀏覽器外,所有瀏覽器都以 WebKit 為基礎。我會以開放原始碼瀏覽器 Firefox、Chrome 和 Safari (部分開放原始碼) 為例。根據 StatCounter 統計資料 (截至 2013 年 6 月),Chrome、Firefox 和 Safari 約占全球電腦版瀏覽器使用量的 71%。在行動裝置上,Android 瀏覽器、iPhone 和 Chrome 的使用率約為 54%。
瀏覽器的主要功能
瀏覽器的主要功能是向伺服器要求你選擇的網路資源,並在瀏覽器視窗中顯示。資源通常是 HTML 文件,但也可能是 PDF、圖片或其他類型的內容。 使用者會透過 URI (統一資源識別碼) 指定資源位置。
瀏覽器解讀及顯示 HTML 檔案的方式,取決於 HTML 和 CSS 規格。這些規格由 W3C (全球資訊網協會) 維護,該組織是網路標準組織。多年來,瀏覽器僅符合部分規格,並開發自己的擴充功能。這對網頁作者造成嚴重的相容性問題。目前大多數瀏覽器或多或少都符合規格。
瀏覽器使用者介面有許多共通點。常見的使用者介面元素包括:
- 用於插入 URI 的網址列
- 「返回」和「前進」按鈕
- 書籤選項
- 「重新整理」和「停止」按鈕,用於重新整理或停止載入目前文件
- 首頁按鈕,可前往首頁
奇怪的是,瀏覽器的使用者介面並未在任何正式規格中指定,而是來自多年經驗和瀏覽器相互模仿而形成的良好做法。HTML5 規格並未定義瀏覽器必須具備的 UI 元素,但列出了一些常見元素。包括網址列、狀態列和工具列。 當然,某些瀏覽器也有獨特功能,例如 Firefox 的下載管理員。
高階基礎架構
瀏覽器的主要元件包括:
- 使用者介面:包括網址列、返回/前進按鈕、書籤選單等。除了顯示所要求網頁的視窗外,瀏覽器顯示的所有部分都屬於使用者介面。
- 瀏覽器引擎:在 UI 和轉譯引擎之間封送處理動作。
- 轉譯引擎:負責顯示要求的內容。舉例來說,如果要求的內容是 HTML,轉譯引擎會剖析 HTML 和 CSS,並在畫面上顯示剖析後的內容。
- 網路:用於 HTTP 要求等網路呼叫,在與平台無關的介面後方,為不同平台使用不同實作。
- UI 後端:用於繪製下拉式方塊和視窗等基本小工具。這個後端會公開一般介面,不限於特定平台。底層則使用作業系統使用者介面方法。
- JavaScript 直譯器。用於剖析及執行 JavaScript 程式碼。
- 資料儲存庫。這是永久層。瀏覽器可能需要在本機儲存各種資料,例如 Cookie。瀏覽器也支援 localStorage、IndexedDB、WebSQL 和 FileSystem 等儲存機制。
請注意,Chrome 等瀏覽器會執行多個轉譯引擎執行個體,每個分頁各一個。每個分頁都會在獨立程序中執行。
轉譯引擎
轉譯引擎的職責是… 轉譯,也就是在瀏覽器畫面上顯示要求的內容。
根據預設,轉譯引擎可以顯示 HTML 和 XML 文件和圖片。瀏覽器可透過外掛程式或擴充功能顯示其他類型的資料,例如使用 PDF 檢視器外掛程式顯示 PDF 文件。不過,本章將著重於主要用途:顯示使用 CSS 格式化的 HTML 和圖片。
不同瀏覽器會使用不同的轉譯引擎:Internet Explorer 使用 Trident、Firefox 使用 Gecko、Safari 使用 WebKit。Chrome 和 Opera (15 以上版本) 則使用 Blink (WebKit 的分支)。
WebKit 是一種開放原始碼轉譯引擎,最初是 Linux 平台的引擎,後來由 Apple 修改,以支援 Mac 和 Windows。
主要流程
轉譯引擎會開始從網路層取得所要求文件的內容。通常會以 8 kB 的區塊執行。
接著,轉譯引擎的基本流程如下:
轉譯引擎會開始剖析 HTML 文件,並將元素轉換為「內容樹狀結構」中的 DOM 節點。引擎會剖析外部 CSS 檔案和樣式元素中的樣式資料。HTML 中的樣式資訊和視覺指示會用來建立另一個樹狀結構:轉譯樹狀結構。
算繪樹狀結構包含具有視覺屬性 (例如顏色和尺寸) 的矩形。矩形會以正確順序顯示在畫面上。
建構轉譯樹狀結構後,會經過「版面配置」程序。也就是為每個節點提供在螢幕上顯示的確切座標。下一個階段是繪製:系統會周遊轉譯樹狀結構,並使用 UI 後端層繪製每個節點。
請注意,這是一個漸進的過程。為了提供更優質的使用者體驗,轉譯引擎會盡快在螢幕上顯示內容。不會等到所有 HTML 都剖析完畢,才開始建構及配置轉譯樹狀結構。系統會剖析並顯示部分內容,同時繼續處理從網路傳入的其他內容。
主要流程範例
從圖 3 和圖 4 可以看出,雖然 WebKit 和 Gecko 使用的術語略有不同,但流程基本上相同。
Gecko 將視覺格式化元素的樹狀結構稱為「框架樹狀結構」。每個元素都是一個影格。WebKit 使用「轉譯樹狀結構」一詞,由「轉譯物件」組成。WebKit 使用「版面配置」一詞來表示元素的位置,而 Gecko 則稱為「重排」。「Attachment」是 WebKit 的術語,用於連結 DOM 節點和視覺資訊,建立轉譯樹狀結構。Gecko 在 HTML 和 DOM 樹狀結構之間多了一層,這是非語意上的細微差異。這個物件稱為「內容接收器」,是建立 DOM 元素的工廠。我們將討論流程的各個部分:
剖析 - 一般
由於剖析是轉譯引擎中非常重要的程序,因此我們會深入探討。首先,我們來簡單介紹剖析。
剖析文件是指將文件轉譯為程式碼可使用的結構。剖析結果通常是代表文件結構的節點樹狀結構。這稱為剖析樹狀結構或語法樹狀結構。
舉例來說,剖析運算式 2 + 3 - 1 可能會傳回以下樹狀結構:
文法
剖析作業會根據文件遵循的語法規則 (即文件編寫時使用的語言或格式) 進行。可剖析的每個格式都必須有確定性文法,由詞彙和語法規則組成。這稱為上下文無關文法。人類語言並非這類語言,因此無法使用傳統剖析技術剖析。
剖析器與詞彙分析器組合
剖析可分為兩個子程序:詞彙分析和語法分析。
詞彙分析是將輸入內容拆解為詞元的程序。詞元是語言詞彙,也就是有效建構區塊的集合。以人類語言來說,這會包含該語言字典中出現的所有字詞。
語法分析是指套用語言語法規則。
剖析器通常會將工作分配給兩個元件:詞法分析器 (有時稱為權杖化工具),負責將輸入內容拆分為有效的權杖;剖析器,負責根據語言語法規則分析文件結構,建構剖析樹。
詞法分析器知道如何去除空格和換行等不相關的字元。
剖析程序會反覆進行。剖析器通常會向詞法分析器要求新的權杖,並嘗試將權杖與其中一項語法規則進行比對。如果符合規則,系統會將對應權杖的節點新增至剖析樹狀結構,剖析器則會要求另一個權杖。
如果沒有相符的規則,剖析器會將權杖儲存在內部,並持續要求權杖,直到找到符合所有內部儲存權杖的規則為止。如果找不到規則,剖析器會引發例外狀況。這表示文件無效,且含有語法錯誤。
翻譯
在許多情況下,剖析樹狀結構並非最終產品。剖析通常用於翻譯:將輸入文件轉換為其他格式。例如,編譯。編譯器會將原始碼編譯成機器碼,首先會將原始碼剖析成剖析樹狀結構,然後將樹狀結構轉譯成機器碼文件。
剖析範例
在圖 5 中,我們根據數學運算式建構剖析樹狀結構。我們來試著定義簡單的數學語言,並查看剖析程序。
語法:
- 語言語法的構成要素包括運算式、詞彙和運算。
- 我們的語言可以包含任意數量的運算式。
- 運算式的定義是「詞彙」後面加上「運算」,再接另一個詞彙
- 運算子是加號或減號符記
- 項是整數符記或運算式
我們來分析輸入內容 2 + 3 - 1。
第一個符合規則的子字串是 2:根據規則 #5,這是一個字詞。第二個相符項目是 2 + 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 識別為運算式 (識別運算式的程序會演變,並比對其他規則,但起點是最高層級規則)。
由下而上剖析器會掃描輸入內容,直到找到相符的規則。然後將相符的輸入內容替換為規則。這個過程會持續到輸入內容結束為止。部分相符的運算式會放在剖析器的堆疊中。
這種由下而上的剖析器稱為移位歸約剖析器,因為輸入內容會向右移位 (想像指標先指向輸入內容的開頭,然後向右移動),並逐漸歸約為語法規則。
自動產生剖析器
有些工具可以產生剖析器。您提供語言的文法 (詞彙和語法規則),它們就會產生可運作的剖析器。建立剖析器需要深入瞭解剖析,而且手動建立最佳化剖析器並不容易,因此剖析器產生器非常實用。
WebKit 使用兩個知名的剖析器產生器:Flex 用於建立詞法分析器,Bison 用於建立剖析器 (您可能會遇到名稱為 Lex 和 Yacc 的剖析器)。Flex 輸入內容是包含權杖規則運算式定義的檔案。Bison 的輸入內容是 BNF 格式的語言語法規則。
HTML 剖析器
HTML 剖析器的作用是將 HTML 標記剖析為剖析樹。
HTML 文法
HTML 的詞彙和語法是由 W3C 組織制定的規格定義。
正如我們在剖析簡介中所見,文法語法可以使用 BNF 等格式正式定義。
可惜的是,所有傳統剖析器主題都不適用於 HTML (我不是為了好玩才提出這些主題,它們會用於剖析 CSS 和 JavaScript)。HTML 無法輕易透過剖析器所需的上下文無關文法定義。
HTML 有正式的定義格式,也就是 DTD (文件類型定義),但這並非上下文無關文法。
這乍看之下很奇怪,因為 HTML 與 XML 相當接近。市面上有許多 XML 剖析器。HTML 有 XML 變體 (XHTML),兩者有何重大差異?
差別在於 HTML 方法較為「寬容」:可省略特定標記 (系統會隱含新增),或有時可省略開始或結束標記等。整體而言,這是一種「軟性」語法,與 XML 的僵硬和嚴格語法相反。
這個看似微不足道的細節,卻能帶來截然不同的效果。一方面,這是 HTML 如此受歡迎的主要原因:它會原諒你的錯誤,讓網頁作者的生活更輕鬆。另一方面,這也使得編寫正式文法變得困難。總之,HTML 無法輕易由傳統剖析器剖析,因為其文法並非上下文無關。XML 剖析器無法剖析 HTML。
HTML DTD
HTML 定義採用 DTD 格式。這個格式用於定義 SGML 系列的語言。這個格式包含所有允許元素的定義、屬性和階層。如前所述,HTML DTD 不會形成上下文無關文法。
DTD 有幾種變化版本。嚴格模式僅符合規格,但其他模式支援瀏覽器過去使用的標記。目的是與舊版內容回溯相容。目前的嚴格 DTD 位於:www.w3.org/TR/html4/strict.dtd
DOM
輸出樹狀結構 (「剖析樹狀結構」) 是 DOM 元素和屬性節點的樹狀結構。DOM 是文件物件模型的簡稱。它是 HTML 文件的物件表示法,也是 HTML 元素與外部世界 (例如 JavaScript) 的介面。
DOM 與標記幾乎是一對一的關係。例如:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
這個標記會轉譯為下列 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 權杖。演算法會以狀態機器表示。每個狀態都會消耗輸入串流的一或多個字元,並根據這些字元更新下一個狀態。這項決策會受到目前的權杖化狀態和樹狀結構建構狀態影響。這表示根據目前狀態,相同的已消耗字元會產生不同的正確下一個狀態結果。演算法太過複雜,無法完整說明,因此我們來看一個簡單的例子,幫助我們瞭解原理。
基本範例 - 標記下列 HTML:
<html>
<body>
Hello world
</body>
</html>
初始狀態為「資料狀態」。遇到 < 字元時,狀態會變更為「Tag open state」。使用 a-z 字元會建立「Start tag token」,狀態會變更為「Tag name state」。我們會維持這個狀態,直到 > 字元用完為止。每個字元都會附加至新的權杖名稱。在本範例中,建立的權杖是 html 權杖。
到達 > 標記時,系統會發出目前的權杖,並將狀態變更回「資料狀態」。<body> 標記也會按照相同步驟處理。目前為止,系統已發出 html 和 body 標記。現在回到「資料狀態」。使用 Hello world 的 H 字元會導致建立及發出字元權杖,直到達到 </body> 的 < 為止。我們會為 Hello world 的每個字元發出字元符記。
現在我們回到「Tag open state」(代碼開啟狀態)。使用下一個輸入內容 / 會建立 end tag token,並移至「Tag name state」。同樣地,我們會保持這個狀態,直到達到 >。接著,系統會發出新的標記權杖,並返回「資料狀態」。</html> 輸入內容的處理方式與前一個案例相同。
樹狀結構建構演算法
剖析器建立後,系統會建立 Document 物件。在樹狀結構建構階段,系統會修改以 Document 為根節點的 DOM 樹狀結構,並在其中加入元素。樹狀結構建構工具會處理權杖化工具產生的每個節點。規格會定義與每個權杖相關的 DOM 元素,並為該權杖建立該元素。元素會新增至 DOM 樹狀結構,以及開啟元素的堆疊。這個堆疊用於修正巢狀結構不符和未關閉的標記。演算法也稱為狀態機器。這些狀態稱為「插入模式」。
以下是範例輸入內容的樹狀結構建構程序:
<html>
<body>
Hello world
</body>
</html>
樹狀結構建構階段的輸入內容,是來自詞元化階段的詞元序列。第一種是「初始模式」。收到「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」模式。收到檔案結尾符記時,剖析作業就會結束。
剖析完成時執行的動作
在這個階段,瀏覽器會將文件標示為互動式,並開始剖析處於「延遲」模式的指令碼:這些指令碼應在文件剖析後執行。接著,文件狀態會設為「完成」,並觸發「載入」事件。
您可以在 HTML5 規格中查看完整的權杖化和樹狀結構演算法。
瀏覽器的容錯能力
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 (我是否漏掉了任何標記?)。
- 我們想在內嵌元素中加入區塊元素。關閉所有內嵌元素,直到下一個較高的區塊元素。
- 如果問題仍未解決,請關閉元素,直到系統允許新增元素為止,或忽略標記。
以下列舉幾個 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);
}
標記階層過深
這則留言不言自明。
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 結尾標記位置不正確
再次強調,留言內容不言自明。
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」是 ID 的簡稱,例如類別名稱。「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 代表空格)。 規則集包含大括號,大括號內則包含一個宣告,或多個以分號分隔的宣告。「declaration」和「selector」將在下列 BNF 定義中定義。
WebKit CSS 剖析器
WebKit 使用 Flex 和 Bison 剖析器產生器,自動從 CSS 文法檔案建立剖析器。您可能還記得剖析器簡介中的內容,Bison 會建立由下而上的移位歸約剖析器。Firefox 則使用手動編寫的由上而下剖析器。無論是哪種情況,每個 CSS 檔案都會剖析為 StyleSheet 物件。每個物件都包含 CSS 規則。CSS 規則物件包含選取器和宣告物件,以及其他對應至 CSS 語法的物件。
指令碼和樣式表的處理順序
指令碼
網路模型是同步的。作者預期剖析器到達 <script> 標記時,指令碼會立即剖析並執行。文件剖析作業會暫停,直到指令碼執行完畢。如果指令碼是外部指令碼,則必須先從網路擷取資源,這也是同步完成,剖析作業會暫停,直到擷取資源為止。這個模式已沿用多年,也列在 HTML4 和 5 規格中。作者可以在指令碼中加入「defer」屬性,這樣指令碼就不會停止文件剖析,並會在文件剖析後執行。HTML5 新增了將指令碼標示為非同步的選項,因此指令碼會由不同的執行緒剖析及執行。
推測剖析
WebKit 和 Firefox 都會進行這項最佳化。執行指令碼時,另一個執行緒會剖析文件的其餘部分,找出需要從網路載入的其他資源,並載入這些資源。這樣一來,資源就能透過平行連線載入,整體速度也會提升。注意:推測剖析器只會剖析外部資源的參照,例如外部指令碼、樣式表和圖片,不會修改 DOM 樹狀結構,這項工作會交由主要剖析器處理。
樣式表
另一方面,樣式表則有不同的模型。從概念上來說,由於樣式表不會變更 DOM 樹狀結構,因此沒有理由等待樣式表並停止文件剖析。不過,在文件剖析階段,指令碼要求樣式資訊時會發生問題。如果樣式尚未載入和剖析,指令碼就會取得錯誤的答案,這顯然會造成許多問題。這看似極端案例,但其實相當常見。如果樣式表仍在載入和剖析,Firefox 會封鎖所有指令碼。只有在指令碼嘗試存取可能受未載入樣式表影響的特定樣式屬性時,WebKit 才會封鎖指令碼。
轉譯樹狀結構建構
在建構 DOM 樹狀結構時,瀏覽器會建構另一個樹狀結構,也就是轉譯樹狀結構。這個樹狀結構會依顯示順序呈現視覺元素。這是文件的視覺化呈現。這個樹狀結構的目的是讓系統能按照正確順序繪製內容。
Firefox 將轉譯樹狀結構中的元素稱為「框架」。WebKit 使用的術語是轉譯器或轉譯物件。
轉譯器知道如何配置及繪製本身和其子項。
WebKit 的 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」元素。此外,顯示值指派為「none」的元素不會出現在樹狀結構中 (但可見度為「hidden」的元素會出現在樹狀結構中)。
DOM 元素會對應到多個視覺物件。這類元素通常結構複雜,無法以單一矩形描述。舉例來說,「select」元素有三個轉譯器:一個用於顯示區域,一個用於下拉式清單方塊,一個用於按鈕。此外,如果文字因寬度不足而分成多行,新行會新增為額外的轉譯器。
另一個多重轉譯器的例子是損毀的 HTML。根據 CSS 規格,內嵌元素只能包含區塊元素或內嵌元素。如果是混合內容,系統會建立匿名區塊轉譯器,包裝內嵌元素。
部分算繪物件對應至 DOM 節點,但不在樹狀結構中的相同位置。浮動和絕對定位元素會脫離流程,放置在樹狀結構的不同部分,並對應至實際框架。原本的位置會顯示預留位置框架。
建構樹狀結構的流程
在 Firefox 中,簡報會註冊為 DOM 更新的事件監聽器。呈現作業會將頁框建立作業委派給 FrameConstructor,而建構函式會解析樣式 (請參閱樣式計算) 並建立頁框。
在 WebKit 中,解析樣式和建立轉譯器的程序稱為「附加」。每個 DOM 節點都有「attach」方法。附加作業是同步進行,將節點插入 DOM 樹狀結構時,會呼叫新節點的「attach」方法。
處理 html 和 body 標記會建構轉譯樹狀結構根。根算繪物件對應至 CSS 規格所稱的包含區塊:包含所有其他區塊的最頂層區塊。其尺寸為可視區域:瀏覽器視窗顯示區域尺寸。Firefox 稱為 ViewPortFrame,WebKit 則稱為 RenderView。這是文件指向的算繪物件。樹狀結構的其餘部分則是以 DOM 節點插入的形式建構。
請參閱處理模型的 CSS2 規格。
樣式運算
建構轉譯樹狀結構時,必須計算每個轉譯物件的視覺屬性。方法是計算每個元素的樣式屬性。
樣式包括各種來源的樣式表、內嵌樣式元素和 HTML 中的視覺屬性 (例如「bgcolor」屬性)。後者會轉譯為相符的 CSS 樣式屬性。
樣式表的來源包括瀏覽器的預設樣式表、網頁作者提供的樣式表,以及使用者樣式表 (由瀏覽器使用者提供的樣式表,瀏覽器可讓您定義偏好的樣式。例如在 Firefox 中,只要將樣式表放入「Firefox 設定檔」資料夾即可。
樣式運算會帶來一些難題:
- 樣式資料是非常龐大的建構,包含許多樣式屬性,這可能會導致記憶體問題。
如果未經最佳化,為每個元素尋找相符規則可能會導致效能問題。為每個元素遍歷整個規則清單來找出相符項目,是一項繁重的工作。選取器可能具有複雜的結構,導致比對程序從看似有希望的路徑開始,但後來證明無效,必須嘗試其他路徑。
舉例來說,這個複合選取器:
div div div div{ ... }表示規則適用於 3 個 div 的子系
<div>。假設您想檢查規則是否適用於特定<div>元素。您選擇樹狀結構中的特定路徑進行檢查。您可能需要向上遍歷節點樹,才能發現只有兩個 div,且規則不適用。接著,您需要嘗試樹狀結構中的其他路徑。套用規則時,會涉及相當複雜的串聯規則,這些規則會定義規則的階層。
一起來看看瀏覽器如何因應這些問題:
分享樣式資料
WebKit 節點會參照樣式物件 (RenderStyle)。在某些情況下,節點可以共用這些物件。節點為同層或同層的子節點,且:
- 元素必須處於相同的滑鼠狀態 (例如,不能一個處於 :hover 狀態,另一個則否)
- 兩個元素都不應有 ID
- 標記名稱應相符
- 類別屬性應相符
- 對應屬性組合必須相同
- 連結狀態必須相符
- 焦點狀態必須相符
- 這兩個元素都不應受到屬性選取器的影響,其中「受影響」的定義是,在選取器中的任何位置,有任何選取器比對使用屬性選取器
- 元素不得有內嵌樣式屬性
- 不得使用任何同層選取器。WebCore 只要遇到任何同層選取器,就會擲回全域切換,並在出現時停用整個文件的樣式共用。包括 + 選取器,以及 :first-child 和 :last-child 等選取器。
Firefox 規則樹狀結構
Firefox 有兩個額外的樹狀結構,可簡化樣式計算:規則樹和樣式內容樹。WebKit 也有樣式物件,但不會儲存在樣式結構定義樹狀結構等樹狀結構中,只有 DOM 節點會指向相關樣式。
樣式情境包含結束值。系統會依正確順序套用所有相符規則,並執行操作,將邏輯值轉換為具體值,藉此計算出這些值。舉例來說,如果邏輯值是螢幕百分比,系統會計算並轉換為絕對單位。規則樹的概念非常聰明。這項功能可讓節點共用這些值,避免重複計算。這也能節省空間。
所有相符的規則都會儲存在樹狀結構中。路徑底部的節點優先順序較高。樹狀圖包含找到的所有規則比對路徑。系統會延遲儲存規則。樹狀結構不會在每個節點的開頭計算,但只要需要計算節點樣式,就會將計算出的路徑新增至樹狀結構。
這個方法是將樹狀結構路徑視為詞彙表中的字詞。假設我們已計算出這個規則樹狀結構:
假設我們需要比對內容樹狀結構中另一個元素的規則,並找出相符的規則 (依正確順序排列) 為 B-E-I。由於我們已計算過路徑 A-B-E-I-L,因此樹狀結構中已有這條路徑。現在我們的工作量減少了。
來看看樹狀結構如何節省工作量。
劃分為 struct
樣式結構定義會分成多個結構。這些結構體包含特定類別 (例如邊框或顏色) 的樣式資訊。struct 中的所有屬性都是繼承或非繼承。繼承屬性是指除非由元素定義,否則會從父項繼承的屬性。未定義的非繼承屬性 (稱為「重設」屬性) 會使用預設值。
樹狀結構會將整個結構體 (包含計算出的最終值) 儲存在樹狀結構中,如果底部節點未提供結構定義,則可使用上層節點中快取的結構。
使用規則樹狀結構計算樣式背景資訊
計算特定元素的樣式內容時,我們會先計算規則樹狀結構中的路徑,或使用現有路徑。接著,我們開始套用路徑中的規則,填入新樣式內容中的結構體。我們從路徑的底部節點開始,也就是優先順序最高的節點 (通常是最具體的選取器),然後向上遍歷樹狀結構,直到結構填滿為止。 如果該規則節點中沒有 struct 的規格,我們就能大幅最佳化,也就是向上搜尋樹狀結構,直到找到完整指定該 struct 的節點並指向該節點,這是最佳的最佳化方式,整個 struct 都會共用。這樣可以節省結束值和記憶體的運算。
如果找到部分定義,我們會向上搜尋樹狀結構,直到填滿結構體。
如果找不到 struct 的定義,且 struct 是「繼承」類型,我們會指向情境樹狀結構中父項的 struct。在這個例子中,我們也成功共用了結構體。如果是重設結構體,則會使用預設值。
如果最明確的節點確實會新增值,我們就需要進行一些額外計算,將其轉換為實際值。 接著,我們將結果快取在樹狀結構節點中,以便子節點使用。
如果某個元素有同層級元素或同層級元素指向同一個樹狀結構節點,則兩者之間可以共用整個樣式結構定義。
我們來看看範例: 假設我們有這個 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}
為簡化流程,假設我們只需要填寫兩個結構體:顏色結構體和邊界結構體。顏色結構體只包含一個成員:顏色 邊界結構體包含四個邊。
產生的規則樹狀結構如下所示 (節點會標示節點名稱:指向的規則編號):
情境樹狀結構會像這樣 (節點名稱:指向的規則節點):
假設我們剖析 HTML 並取得第二個 <div> 標記。我們需要為這個節點建立樣式結構定義,並填入樣式結構。
系統會比對規則,並發現 <div> 的比對規則為 1、2 和 6。這表示樹狀結構中已有元素可使用的路徑,我們只需要為規則 6 新增另一個節點 (規則樹狀結構中的節點 F)。
我們會建立樣式背景資訊,並將其放入背景資訊樹狀結構中。新的樣式內容會指向規則樹狀結構中的節點 F。
現在需要填入樣式結構。首先,我們要填寫邊界結構。由於最後一個規則節點 (F) 不會新增至邊界結構,因此我們可以向上移動樹狀結構,直到找到在先前節點插入中計算的快取結構,並使用該結構。我們會在節點 B 找到這個屬性,因為節點 B 是指定邊界規則的最上層節點。
我們確實有顏色結構體的定義,因此無法使用快取的結構體。由於顏色只有一個屬性,因此我們不需要向上移動樹狀結構來填入其他屬性。我們會計算結束值 (將字串轉換為 RGB 等),並在這個節點上快取計算出的結構。
第二個 <span> 元素的工作更簡單。我們會比對規則,並得出結論:它指向規則 G,就像前一個 span 一樣。由於同層元素指向同一個節點,因此我們可以共用整個樣式結構定義,並指向前一個 span 的結構定義。
對於包含繼承自父項規則的結構,快取會在內容樹狀結構上完成 (顏色屬性實際上是繼承的,但 Firefox 會將其視為重設,並在規則樹狀結構上快取)。
舉例來說,如果我們為段落中的字型新增規則:
p {font-family: Verdana; font size: 10px; font-weight: bold}
接著,段落元素 (在結構定義樹狀結構中是 div 的子項) 就能與父項共用相同的字型結構體。如果未指定段落的字型規則,就會發生這種情況。
在沒有規則樹的 WebKit 中,相符的宣告會遍歷四次。首先套用非重要的高優先順序屬性 (應優先套用的屬性,因為其他屬性依賴這些屬性,例如 display),然後是高優先順序的重要屬性,接著是一般優先順序的非重要屬性,最後是一般優先順序的重要規則。這表示出現多次的屬性會根據正確的階層順序解析。以最後一個為準。
總結來說,共用樣式物件 (全部或部分內部結構) 可解決問題 1 和 3。Firefox 規則樹狀結構也有助於以正確順序套用屬性。
操弄規則,輕鬆配對
樣式規則有幾個來源:
- CSS 規則,無論是在外部樣式表或樣式元素中。
css p {color: blue} - 內嵌樣式屬性,例如
html <p style="color: blue" /> - HTML 視覺屬性 (對應至相關樣式規則)
html <p bgcolor="blue" />最後兩項很容易與元素比對,因為元素擁有樣式屬性,且 HTML 屬性可以使用元素做為索引鍵進行對應。
如先前在問題 #2 中所述,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 元素相符。
WebKit 和 Firefox 都會執行這項操作。
樣式表串聯順序
樣式物件具有對應每個視覺屬性的屬性 (所有 CSS 屬性,但更通用)。如果屬性未由任何相符規則定義,則部分屬性可由父項元素樣式物件繼承。其他屬性則有預設值。
當有多個定義時,問題就會開始,這裡有解決問題的階層順序。
樣式屬性的宣告可以出現在多個樣式表中,也可以在樣式表中出現多次。這表示套用規則的順序非常重要。這稱為「階層」順序。根據 CSS2 規格,階層順序 (由低到高) 如下:
- 瀏覽器宣告
- 使用者一般宣告
- 編寫一般宣告
- 撰寫重要聲明
- 使用者重要聲明
瀏覽器宣告最不重要,只有在宣告標示為重要時,使用者才會覆寫作者。 如果宣告的順序相同,系統會依特異性排序,然後再依指定的順序排序。HTML 視覺屬性會轉譯為相符的 CSS 宣告。系統會將其視為優先順序較低的作者規則。
優先權
選取器特異性由 CSS2 規格定義,如下所示:
- 如果宣告來自「style」屬性,而非含有選取器的規則,則計為 1,否則為 0 (= a)
- 計算選取器中的 ID 屬性數量 (= b)
- 計算選取器中其他屬性和虛擬類別的數量 (= c)
- 計算選取器中的元素名稱和虛擬元素數量 (= d)
將 a-b-c-d 四個數字串連起來 (在底數較大的數字系統中),即可得出特異性。
您需要使用的數字基數,取決於其中一個類別的最高計數。
舉例來說,如果 a=14,您可以使用十六進位制。在極少數情況下,如果 a=17,則需要 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 表格可能需要多次傳遞。
座標系統與根框架相關。使用頂端和左側座標。
版面配置是遞迴程序。這個程序會從根轉譯器開始,對應至 HTML 文件的 <html> 元素。版面配置會透過部分或所有框架階層遞迴,為每個需要幾何資訊的轉譯器計算幾何資訊。
根轉譯器的位置為 0,0,尺寸為可視區域 (瀏覽器視窗的可見部分)。
所有轉譯器都有「版面配置」或「重排」方法,每個轉譯器都會叫用需要版面配置的子項版面配置方法。
Dirty bit 系統
為了避免每次微幅變更都要重新進行完整版面配置,瀏覽器會使用「dirty bit」系統。變更或新增的轉譯器會將自身和子項標示為「dirty」,表示需要版面配置。
有兩個標記:「dirty」和「children are dirty」,後者表示雖然轉譯器本身可能沒問題,但至少有一個子項需要版面配置。
全域和遞增版面配置
版面配置可以在整個轉譯樹狀結構上觸發,這稱為「全域」版面配置。可能原因如下:
- 影響所有轉譯器的全域樣式變更,例如字型大小變更。
- 螢幕大小調整
版面配置可以遞增,只有髒轉譯器會進行版面配置 (這可能會造成一些損壞,需要額外的版面配置)。
當轉譯器不乾淨時,系統會觸發增量版面配置 (非同步)。例如,當額外內容來自網路並新增至 DOM 樹狀結構後,新的轉譯器會附加至轉譯樹狀結構。
非同步和同步版面配置
增量版面配置會以非同步方式完成。Firefox 會將「重排指令」排入佇列,用於遞增版面配置,並由排程器觸發這些指令的批次執行。 WebKit 也有計時器,可執行遞增版面配置,也就是周遊樹狀結構,並配置「髒」轉譯器。
要求樣式資訊 (例如「offsetHeight」) 的指令碼,可能會同步觸發遞增版面配置。
全域版面配置通常會同步觸發。
有時,由於捲動位置等屬性變更,系統會在初始版面配置後觸發版面配置做為回呼。
最佳化
如果版面配置是由「調整大小」或轉譯器位置(而非大小) 變更觸發,系統會從快取擷取轉譯器大小,而不會重新計算…
在某些情況下,只有子樹會遭到修改,版面配置不會從根層級開始。如果變更發生在局部,且不會影響周圍環境,就可能發生這種情況,例如在文字欄位中插入文字 (否則每次按鍵都會觸發從根層級開始的版面配置)。
版面配置程序
版面配置通常採用以下模式:
- 父項轉譯器會決定自己的寬度。
- 父項會檢查子項,並執行以下操作:
- 放置子項轉譯器 (設定其 x 和 y)。
- 視需要呼叫子項版面配置 (例如子項已變更、我們處於全域版面配置,或基於其他原因),計算子項的高度。
- 父項會使用子項的累計高度,以及邊界和邊框間距的高度來設定自身高度,這會由父項轉譯器的父項使用。
- 將其 dirty bit 設為 false。
Firefox 使用「狀態」物件(nsHTMLReflowState) 做為版面配置 (稱為「重排」) 的參數。其中,狀態包含父項寬度。
Firefox 版面配置的輸出內容是「指標」物件(nsHTMLReflowMetrics)。其中會包含轉譯器計算的高度。
寬度計算
系統會根據容器區塊的寬度、轉譯器的樣式「width」屬性、邊界和邊框,計算轉譯器的寬度。
例如,以下 div 的寬度:
<div style="width: 30%"/>
WebKit 會計算如下(類別 RenderBox 方法 calcWidth):
- 容器寬度是容器 availableWidth 和 0 的最大值。在本例中,availableWidth 是 contentWidth,計算方式如下:
clientWidth() - paddingLeft() - paddingRight()
clientWidth 和 clientHeight 代表物件內部,不含邊框和捲軸。
元素寬度是「width」樣式屬性。系統會計算容器寬度的百分比,並以絕對值表示。
現在已新增水平框線和邊框間距。
到目前為止,我們計算的是「偏好寬度」。現在,系統會計算寬度下限和上限。
如果偏好寬度大於最大寬度,則會使用最大寬度。如果小於最小寬度 (最小不可分割單位),則會使用最小寬度。
系統會快取這些值,以備需要版面配置時使用,但寬度不會改變。
斷行
當版面配置中間的轉譯器決定需要中斷時,轉譯器會停止,並向版面配置的父項傳播需要中斷。父項會建立額外的轉譯器,並對其呼叫版面配置。
繪畫
在繪製階段,系統會遍歷轉譯樹狀結構,並呼叫轉譯器的「paint()」方法,在畫面上顯示內容。繪製作業會使用 UI 基礎架構元件。
全域和增量
與版面配置一樣,繪製也可以是全域 (繪製整個樹狀結構) 或遞增。在增量繪製中,部分轉譯器的變更方式不會影響整個樹狀結構。變更後的轉譯器會使螢幕上的矩形失效。這會導致 OS 將其視為「髒區域」,並產生「繪製」事件。OS 會巧妙地將多個區域合併為一個。在 Chrome 中,這項作業較為複雜,因為轉譯器與主程序位於不同程序。Chrome 會在一定程度上模擬 OS 行為。簡報會監聽這些事件,並將訊息委派給轉譯根層級。系統會周遊樹狀結構,直到找到相關的轉譯器。它會重新繪製自身 (通常也包括子項)。
繪製順序
CSS2 定義了繪製程序的順序。這實際上是元素在堆疊內容中的堆疊順序。這個順序會影響繪製,因為堆疊是從後到前繪製。區塊轉譯器的堆疊順序如下:
- 背景顏色
- 背景圖片
- border
- 孩子
- 大綱
Firefox 顯示清單
Firefox 會檢查轉譯樹狀結構,並為繪製的矩形建立顯示清單。其中包含與矩形相關的轉譯器,並按照正確的繪製順序 (轉譯器的背景、邊框等) 排列。
這樣一來,樹狀結構只需要遍歷一次即可重新繪製,而不必多次遍歷,例如繪製所有背景、所有圖片、所有邊框等。
Firefox 會最佳化程序,不加入會隱藏的元素,例如完全位於其他不透明元素下方的元素。
WebKit 矩形儲存空間
重新繪製前,WebKit 會將舊矩形儲存為點陣圖。接著,系統只會繪製新舊矩形之間的差異。
動態變更
瀏覽器會盡可能減少因應變更而採取的動作。因此,變更元素顏色只會重新繪製該元素。變更元素位置會導致元素、其子項和同層級元素重新配置版面和重新繪製。 新增 DOM 節點會導致節點的版面配置和重新繪製。 重大變更 (例如增加「html」元素的字型大小) 會導致快取失效、重新版面配置和重新繪製整個樹狀結構。
轉譯引擎的執行緒
算繪引擎為單一執行緒。除了網路作業外,幾乎所有作業都會在單一執行緒中進行。在 Firefox 和 Safari 中,這是瀏覽器的主執行緒。在 Chrome 中,這是分頁程序主執行緒。
網路作業可由多個平行執行緒執行。平行連線數量有限 (通常為 2 到 6 個連線)。
事件迴圈
瀏覽器主執行緒是事件迴圈。這是一個無限迴圈,可讓程序保持運作。它會等待事件 (例如版面配置和繪製事件) 並進行處理。以下是主要事件迴圈的 Firefox 程式碼:
while (!mExiting)
NS_ProcessNextEvent(thread);
CSS2 視覺模型
畫布
根據 CSS2 規格,「畫布」一詞是指「轉譯格式結構的空間」:瀏覽器繪製內容的位置。
畫布在空間的每個維度上都是無限的,但瀏覽器會根據可視區域的尺寸選擇初始寬度。
根據 www.w3.org/TR/CSS2/zindex.html,如果畫布包含在另一個畫布中,則畫布為透明,如果不是,則會指定瀏覽器定義的顏色。
CSS 盒模型
CSS 盒模型描述了為文件樹狀結構中的元素產生的矩形盒,並根據視覺格式模型進行版面配置。
每個方塊都有內容區域 (例如文字、圖片等),以及選用的周圍邊框間距、邊框和邊界區域。
每個節點會產生 0 到 n 個這類方塊。
所有元素都有「display」屬性,可決定要產生的方塊類型。
範例:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
預設值為 inline,但瀏覽器樣式表可能會設定其他預設值。舉例來說,「div」元素的預設顯示方式為區塊。
如要查看預設樣式表範例,請前往 www.w3.org/TR/CSS2/sample.html。
定位配置
共有三種配置:
- 一般:根據物件在文件中的位置來定位。這表示它在轉譯樹狀結構中的位置與在 DOM 樹狀結構中的位置相同,並根據其方塊類型和尺寸進行版面配置
- 浮動:物件會先按照一般流程排列,然後盡可能向左或向右移動
- 絕對:物件在轉譯樹狀結構中的位置與在 DOM 樹狀結構中的位置不同
定位配置是由「position」屬性和「float」屬性設定。
- static 和 relative 會造成正常流程
- absolute 和 fixed 會導致絕對定位
靜態定位不會定義任何位置,而是使用預設定位。在其他配置中,作者會指定位置:頂端、底部、左側、右側。
方塊的版面配置方式取決於:
- Box 類型
- 箱子尺寸
- 定位配置
- 外部資訊,例如圖片大小和螢幕大小
Box 類型
區塊盒:形成區塊,在瀏覽器視窗中擁有自己的矩形。
內嵌方塊:沒有自己的區塊,但位於包含區塊內。
區塊會垂直排列,一個接一個。內嵌廣告會以橫向格式顯示。
內嵌方塊會放在行或「行方塊」中。線條的高度至少與最高的方塊相同,但如果方塊對齊「基準」(即元素的底部與另一個方塊的底部以外的點對齊),則線條的高度可能會更高。 如果容器寬度不足,內嵌廣告會分成多行顯示。 這通常是段落中會發生的情況。
位置
相對時間
相對定位:照常定位,然後依所需差異移動。
浮動
浮動方塊會移至一行的左側或右側。有趣的是,其他方塊會圍繞著它流動。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. 網路瀏覽器參考架構 (PDF)
- Gupta, Vineet. 瀏覽器運作方式 - 第 1 部分 - 架構
剖析
- Aho、Sethi、Ullman,《編譯器:原理、技術和工具》(又稱「龍書」),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 網頁程式開發人員。
- L. David Baron,Faster HTML and CSS: Layout Engine Internals for 網頁程式開發人員 (Google 科技講座 video)
- L. David Baron,Mozilla 的版面配置引擎
- L. David Baron,Mozilla 樣式系統說明文件
- Chris Waterson,HTML 重新排列注意事項
- Chris Waterson,Gecko 總覽
- Alexander Larsson,HTML HTTP 要求的生命週期
WebKit
- David Hyatt,Implementing CSS(part 1)
- David Hyatt,WebCore 總覽
- David Hyatt,WebCore 轉譯
- David Hyatt,FOUC 問題
W3C 規格
瀏覽器建構操作說明
翻譯
這個網頁已翻譯成日文兩次:
- 瀏覽器運作方式 - 新式網路瀏覽器的幕後運作 (日文) 作者:@kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 by @ikeike443 and @kiyoto01.
感謝大家!