瀏覽器的運作方式

現代網路瀏覽器的幕後故事

前言

這份關於 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 (全球資訊網協會) 維護,該組織是網路標準組織。多年來,瀏覽器僅符合部分規格,並開發自己的擴充功能。這對網頁作者造成嚴重的相容性問題。目前大多數瀏覽器或多或少都符合規格。

瀏覽器使用者介面有許多共通點。常見的使用者介面元素包括:

  1. 用於插入 URI 的網址列
  2. 「返回」和「前進」按鈕
  3. 書籤選項
  4. 「重新整理」和「停止」按鈕,用於重新整理或停止載入目前文件
  5. 首頁按鈕,可前往首頁

奇怪的是,瀏覽器的使用者介面並未在任何正式規格中指定,而是來自多年經驗和瀏覽器相互模仿而形成的良好做法。HTML5 規格並未定義瀏覽器必須具備的 UI 元素,但列出了一些常見元素。包括網址列、狀態列和工具列。 當然,某些瀏覽器也有獨特功能,例如 Firefox 的下載管理員。

高階基礎架構

瀏覽器的主要元件包括:

  1. 使用者介面:包括網址列、返回/前進按鈕、書籤選單等。除了顯示所要求網頁的視窗外,瀏覽器顯示的所有部分都屬於使用者介面。
  2. 瀏覽器引擎:在 UI 和轉譯引擎之間封送處理動作。
  3. 轉譯引擎:負責顯示要求的內容。舉例來說,如果要求的內容是 HTML,轉譯引擎會剖析 HTML 和 CSS,並在畫面上顯示剖析後的內容。
  4. 網路:用於 HTTP 要求等網路呼叫,在與平台無關的介面後方,為不同平台使用不同實作。
  5. UI 後端:用於繪製下拉式方塊和視窗等基本小工具。這個後端會公開一般介面,不限於特定平台。底層則使用作業系統使用者介面方法。
  6. JavaScript 直譯器。用於剖析及執行 JavaScript 程式碼。
  7. 資料儲存庫。這是永久層。瀏覽器可能需要在本機儲存各種資料,例如 Cookie。瀏覽器也支援 localStorage、IndexedDB、WebSQL 和 FileSystem 等儲存機制。
瀏覽器元件
圖 1:瀏覽器元件

請注意,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 的區塊執行。

接著,轉譯引擎的基本流程如下:

算繪引擎基本流程
圖 2:轉譯引擎基本流程

轉譯引擎會開始剖析 HTML 文件,並將元素轉換為「內容樹狀結構」中的 DOM 節點。引擎會剖析外部 CSS 檔案和樣式元素中的樣式資料。HTML 中的樣式資訊和視覺指示會用來建立另一個樹狀結構:轉譯樹狀結構。

算繪樹狀結構包含具有視覺屬性 (例如顏色和尺寸) 的矩形。矩形會以正確順序顯示在畫面上。

建構轉譯樹狀結構後,會經過「版面配置」程序。也就是為每個節點提供在螢幕上顯示的確切座標。下一個階段是繪製:系統會周遊轉譯樹狀結構,並使用 UI 後端層繪製每個節點。

請注意,這是一個漸進的過程。為了提供更優質的使用者體驗,轉譯引擎會盡快在螢幕上顯示內容。不會等到所有 HTML 都剖析完畢,才開始建構及配置轉譯樹狀結構。系統會剖析並顯示部分內容,同時繼續處理從網路傳入的其他內容。

主要流程範例

WebKit 主要流程。
圖 3:WebKit 主要流程
Mozilla Gecko 轉譯引擎的主要流程。
圖 4:Mozilla 的 Gecko 轉譯引擎主要流程

從圖 3 和圖 4 可以看出,雖然 WebKit 和 Gecko 使用的術語略有不同,但流程基本上相同。

Gecko 將視覺格式化元素的樹狀結構稱為「框架樹狀結構」。每個元素都是一個影格。WebKit 使用「轉譯樹狀結構」一詞,由「轉譯物件」組成。WebKit 使用「版面配置」一詞來表示元素的位置,而 Gecko 則稱為「重排」。「Attachment」是 WebKit 的術語,用於連結 DOM 節點和視覺資訊,建立轉譯樹狀結構。Gecko 在 HTML 和 DOM 樹狀結構之間多了一層,這是非語意上的細微差異。這個物件稱為「內容接收器」,是建立 DOM 元素的工廠。我們將討論流程的各個部分:

剖析 - 一般

由於剖析是轉譯引擎中非常重要的程序,因此我們會深入探討。首先,我們來簡單介紹剖析。

剖析文件是指將文件轉譯為程式碼可使用的結構。剖析結果通常是代表文件結構的節點樹狀結構。這稱為剖析樹狀結構或語法樹狀結構。

舉例來說,剖析運算式 2 + 3 - 1 可能會傳回以下樹狀結構:

數學運算式樹狀結構節點。
圖 5:數學運算式樹狀結構節點

文法

剖析作業會根據文件遵循的語法規則 (即文件編寫時使用的語言或格式) 進行。可剖析的每個格式都必須有確定性文法,由詞彙和語法規則組成。這稱為上下文無關文法。人類語言並非這類語言,因此無法使用傳統剖析技術剖析。

剖析器與詞彙分析器組合

剖析可分為兩個子程序:詞彙分析和語法分析。

詞彙分析是將輸入內容拆解為詞元的程序。詞元是語言詞彙,也就是有效建構區塊的集合。以人類語言來說,這會包含該語言字典中出現的所有字詞。

語法分析是指套用語言語法規則。

剖析器通常會將工作分配給兩個元件:詞法分析器 (有時稱為權杖化工具),負責將輸入內容拆分為有效的權杖;剖析器,負責根據語言語法規則分析文件結構,建構剖析樹。

詞法分析器知道如何去除空格和換行等不相關的字元。

從來源文件到剖析樹狀結構
圖 6:從來源文件到剖析樹狀結構

剖析程序會反覆進行。剖析器通常會向詞法分析器要求新的權杖,並嘗試將權杖與其中一項語法規則進行比對。如果符合規則,系統會將對應權杖的節點新增至剖析樹狀結構,剖析器則會要求另一個權杖。

如果沒有相符的規則,剖析器會將權杖儲存在內部,並持續要求權杖,直到找到符合所有內部儲存權杖的規則為止。如果找不到規則,剖析器會引發例外狀況。這表示文件無效,且含有語法錯誤。

翻譯

在許多情況下,剖析樹狀結構並非最終產品。剖析通常用於翻譯:將輸入文件轉換為其他格式。例如,編譯。編譯器會將原始碼編譯成機器碼,首先會將原始碼剖析成剖析樹狀結構,然後將樹狀結構轉譯成機器碼文件。

編譯流程
圖 7:編譯流程

剖析範例

在圖 5 中,我們根據數學運算式建構剖析樹狀結構。我們來試著定義簡單的數學語言,並查看剖析程序。

語法:

  1. 語言語法的構成要素包括運算式、詞彙和運算。
  2. 我們的語言可以包含任意數量的運算式。
  3. 運算式的定義是「詞彙」後面加上「運算」,再接另一個詞彙
  4. 運算子是加號或減號符記
  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 識別為運算式 (識別運算式的程序會演變,並比對其他規則,但起點是最高層級規則)。

由下而上剖析器會掃描輸入內容,直到找到相符的規則。然後將相符的輸入內容替換為規則。這個過程會持續到輸入內容結束為止。部分相符的運算式會放在剖析器的堆疊中。

堆疊 輸入
2 + 3 - 1
字詞 + 3 - 1
字詞作業 3 - 1
運算式 - 1
運算式作業 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) 的介面。

樹狀結構的根節點是「Document」物件。

DOM 與標記幾乎是一對一的關係。例如:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

這個標記會轉譯為下列 DOM 樹狀結構:

範例標記的 DOM 樹狀結構
圖 8:範例標記的 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 規格)
圖 9:HTML 剖析流程 (取自 HTML5 規格)

詞元化演算法

演算法的輸出內容是 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> 輸入內容的處理方式與前一個案例相同。

將範例輸入內容權杖化
圖 10:將範例輸入內容權杖化

樹狀結構建構演算法

剖析器建立後,系統會建立 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」模式。收到檔案結尾符記時,剖析作業就會結束。

HTML 範例的樹狀結構。
圖 11:範例 HTML 的樹狀結構

剖析完成時執行的動作

在這個階段,瀏覽器會將文件標示為互動式,並開始剖析處於「延遲」模式的指令碼:這些指令碼應在文件剖析後執行。接著,文件狀態會設為「完成」,並觸發「載入」事件。

您可以在 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. 我們想在內嵌元素中加入區塊元素。關閉所有內嵌元素,直到下一個較高的區塊元素。
  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);
}

標記階層過深

這則留言不言自明。

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 語法的物件。

剖析 CSS。
圖 12:剖析 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 節點,但不在樹狀結構中的相同位置。浮動和絕對定位元素會脫離流程,放置在樹狀結構的不同部分,並對應至實際框架。原本的位置會顯示預留位置框架。

轉譯樹狀結構和對應的 DOM 樹狀結構。
圖 13:轉譯樹狀結構和對應的 DOM 樹狀結構。「可視區域」是初始包含區塊。在 WebKit 中,這會是「RenderView」物件

建構樹狀結構的流程

在 Firefox 中,簡報會註冊為 DOM 更新的事件監聽器。呈現作業會將頁框建立作業委派給 FrameConstructor,而建構函式會解析樣式 (請參閱樣式計算) 並建立頁框。

在 WebKit 中,解析樣式和建立轉譯器的程序稱為「附加」。每個 DOM 節點都有「attach」方法。附加作業是同步進行,將節點插入 DOM 樹狀結構時,會呼叫新節點的「attach」方法。

處理 html 和 body 標記會建構轉譯樹狀結構根。根算繪物件對應至 CSS 規格所稱的包含區塊:包含所有其他區塊的最頂層區塊。其尺寸為可視區域:瀏覽器視窗顯示區域尺寸。Firefox 稱為 ViewPortFrame,WebKit 則稱為 RenderView。這是文件指向的算繪物件。樹狀結構的其餘部分則是以 DOM 節點插入的形式建構。

請參閱處理模型的 CSS2 規格。

樣式運算

建構轉譯樹狀結構時,必須計算每個轉譯物件的視覺屬性。方法是計算每個元素的樣式屬性。

樣式包括各種來源的樣式表、內嵌樣式元素和 HTML 中的視覺屬性 (例如「bgcolor」屬性)。後者會轉譯為相符的 CSS 樣式屬性。

樣式表的來源包括瀏覽器的預設樣式表、網頁作者提供的樣式表,以及使用者樣式表 (由瀏覽器使用者提供的樣式表,瀏覽器可讓您定義偏好的樣式。例如在 Firefox 中,只要將樣式表放入「Firefox 設定檔」資料夾即可。

樣式運算會帶來一些難題:

  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. 元素不得有內嵌樣式屬性
  10. 不得使用任何同層選取器。WebCore 只要遇到任何同層選取器,就會擲回全域切換,並在出現時停用整個文件的樣式共用。包括 + 選取器,以及 :first-child 和 :last-child 等選取器。

Firefox 規則樹狀結構

Firefox 有兩個額外的樹狀結構,可簡化樣式計算:規則樹和樣式內容樹。WebKit 也有樣式物件,但不會儲存在樣式結構定義樹狀結構等樹狀結構中,只有 DOM 節點會指向相關樣式。

Firefox 樣式內容樹狀結構。
圖 14:Firefox 樣式內容樹狀結構。

樣式情境包含結束值。系統會依正確順序套用所有相符規則,並執行操作,將邏輯值轉換為具體值,藉此計算出這些值。舉例來說,如果邏輯值是螢幕百分比,系統會計算並轉換為絕對單位。規則樹的概念非常聰明。這項功能可讓節點共用這些值,避免重複計算。這也能節省空間。

所有相符的規則都會儲存在樹狀結構中。路徑底部的節點優先順序較高。樹狀圖包含找到的所有規則比對路徑。系統會延遲儲存規則。樹狀結構不會在每個節點的開頭計算,但只要需要計算節點樣式,就會將計算出的路徑新增至樹狀結構。

這個方法是將樹狀結構路徑視為詞彙表中的字詞。假設我們已計算出這個規則樹狀結構:

運算完成的規則樹狀結構
圖 15:計算出的規則樹狀結構。

假設我們需要比對內容樹狀結構中另一個元素的規則,並找出相符的規則 (依正確順序排列) 為 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}

為簡化流程,假設我們只需要填寫兩個結構體:顏色結構體和邊界結構體。顏色結構體只包含一個成員:顏色 邊界結構體包含四個邊。

產生的規則樹狀結構如下所示 (節點會標示節點名稱:指向的規則編號):

規則樹
圖 16:規則樹狀結構

情境樹狀結構會像這樣 (節點名稱:指向的規則節點):

背景資訊樹狀結構。
圖 17:情境樹狀結構

假設我們剖析 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 規則樹狀結構也有助於以正確順序套用屬性。

操弄規則,輕鬆配對

樣式規則有幾個來源:

  1. CSS 規則,無論是在外部樣式表或樣式元素中。 css p {color: blue}
  2. 內嵌樣式屬性,例如 html <p style="color: blue" />
  3. 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 規格,階層順序 (由低到高) 如下:

  1. 瀏覽器宣告
  2. 使用者一般宣告
  3. 編寫一般宣告
  4. 撰寫重要聲明
  5. 使用者重要聲明

瀏覽器宣告最不重要,只有在宣告標示為重要時,使用者才會覆寫作者。 如果宣告的順序相同,系統會依特異性排序,然後再依指定的順序排序。HTML 視覺屬性會轉譯為相符的 CSS 宣告。系統會將其視為優先順序較低的作者規則。

優先權

選取器特異性由 CSS2 規格定義,如下所示:

  1. 如果宣告來自「style」屬性,而非含有選取器的規則,則計為 1,否則為 0 (= a)
  2. 計算選取器中的 ID 屬性數量 (= b)
  3. 計算選取器中其他屬性和虛擬類別的數量 (= c)
  4. 計算選取器中的元素名稱和虛擬元素數量 (= 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」,後者表示雖然轉譯器本身可能沒問題,但至少有一個子項需要版面配置。

全域和遞增版面配置

版面配置可以在整個轉譯樹狀結構上觸發,這稱為「全域」版面配置。可能原因如下:

  1. 影響所有轉譯器的全域樣式變更,例如字型大小變更。
  2. 螢幕大小調整

版面配置可以遞增,只有髒轉譯器會進行版面配置 (這可能會造成一些損壞,需要額外的版面配置)。

當轉譯器不乾淨時,系統會觸發增量版面配置 (非同步)。例如,當額外內容來自網路並新增至 DOM 樹狀結構後,新的轉譯器會附加至轉譯樹狀結構。

增量版面配置。
圖 18:增量版面配置 - 僅配置髒渲染器及其子項

非同步和同步版面配置

增量版面配置會以非同步方式完成。Firefox 會將「重排指令」排入佇列,用於遞增版面配置,並由排程器觸發這些指令的批次執行。 WebKit 也有計時器,可執行遞增版面配置,也就是周遊樹狀結構,並配置「髒」轉譯器。

要求樣式資訊 (例如「offsetHeight」) 的指令碼,可能會同步觸發遞增版面配置。

全域版面配置通常會同步觸發。

有時,由於捲動位置等屬性變更,系統會在初始版面配置後觸發版面配置做為回呼。

最佳化

如果版面配置是由「調整大小」或轉譯器位置(而非大小) 變更觸發,系統會從快取擷取轉譯器大小,而不會重新計算…

在某些情況下,只有子樹會遭到修改,版面配置不會從根層級開始。如果變更發生在局部,且不會影響周圍環境,就可能發生這種情況,例如在文字欄位中插入文字 (否則每次按鍵都會觸發從根層級開始的版面配置)。

版面配置程序

版面配置通常採用以下模式:

  1. 父項轉譯器會決定自己的寬度。
  2. 父項會檢查子項,並執行以下操作:
    1. 放置子項轉譯器 (設定其 x 和 y)。
    2. 視需要呼叫子項版面配置 (例如子項已變更、我們處於全域版面配置,或基於其他原因),計算子項的高度。
  3. 父項會使用子項的累計高度,以及邊界和邊框間距的高度來設定自身高度,這會由父項轉譯器的父項使用。
  4. 將其 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 定義了繪製程序的順序。這實際上是元素在堆疊內容中的堆疊順序。這個順序會影響繪製,因為堆疊是從後到前繪製。區塊轉譯器的堆疊順序如下:

  1. 背景顏色
  2. 背景圖片
  3. border
  4. 孩子
  5. 大綱

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 盒模型描述了為文件樹狀結構中的元素產生的矩形盒,並根據視覺格式模型進行版面配置。

每個方塊都有內容區域 (例如文字、圖片等),以及選用的周圍邊框間距、邊框和邊界區域。

CSS2 盒模型
圖 19:CSS2 盒模型

每個節點會產生 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。

定位配置

共有三種配置:

  1. 一般:根據物件在文件中的位置來定位。這表示它在轉譯樹狀結構中的位置與在 DOM 樹狀結構中的位置相同,並根據其方塊類型和尺寸進行版面配置
  2. 浮動:物件會先按照一般流程排列,然後盡可能向左或向右移動
  3. 絕對:物件在轉譯樹狀結構中的位置與在 DOM 樹狀結構中的位置不同

定位配置是由「position」屬性和「float」屬性設定。

  • static 和 relative 會造成正常流程
  • absolute 和 fixed 會導致絕對定位

靜態定位不會定義任何位置,而是使用預設定位。在其他配置中,作者會指定位置:頂端、底部、左側、右側。

方塊的版面配置方式取決於:

  • Box 類型
  • 箱子尺寸
  • 定位配置
  • 外部資訊,例如圖片大小和螢幕大小

Box 類型

區塊盒:形成區塊,在瀏覽器視窗中擁有自己的矩形。

封鎖方塊。
圖 20:區塊盒

內嵌方塊:沒有自己的區塊,但位於包含區塊內。

內嵌方塊。
圖 21:內嵌方塊

區塊會垂直排列,一個接一個。內嵌廣告會以橫向格式顯示。

區塊和內嵌格式。
圖 22:區塊和內嵌格式

內嵌方塊會放在行或「行方塊」中。線條的高度至少與最高的方塊相同,但如果方塊對齊「基準」(即元素的底部與另一個方塊的底部以外的點對齊),則線條的高度可能會更高。 如果容器寬度不足,內嵌廣告會分成多行顯示。 這通常是段落中會發生的情況。

線條。
圖 23:線條

位置

相對時間

相對定位:照常定位,然後依所需差異移動。

相對定位。
圖 24:相對定位

浮動

浮動方塊會移至一行的左側或右側。有趣的是,其他方塊會圍繞著它流動。HTML:

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>

看起來會像這樣:

浮點值。
圖 25:浮點數

絕對和固定

無論一般流程為何,版面配置都會精確定義。元素不會參與正常流程。尺寸是相對於容器。在固定模式中,容器就是可視區域。

固定位置。
圖 26:固定定位

分層表示法

這項屬性是由 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>

結果如下:

固定位置。
圖 27:固定位置

雖然紅色 div 在標記中位於綠色 div 之前,且在一般流程中會先繪製,但 z-index 屬性較高,因此在根方塊保留的堆疊中更靠前。

資源

  1. 瀏覽器架構

    1. Grosskurth, Alan. 網路瀏覽器參考架構 (PDF)
    2. Gupta, Vineet. 瀏覽器運作方式 - 第 1 部分 - 架構
  2. 剖析

    1. Aho、Sethi、Ullman,《編譯器:原理、技術和工具》(又稱「龍書」),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 網頁程式開發人員。
    2. L. David Baron,Faster HTML and CSS: Layout Engine Internals for 網頁程式開發人員 (Google 科技講座 video)
    3. L. David Baron,Mozilla 的版面配置引擎
    4. L. David Baron,Mozilla 樣式系統說明文件
    5. Chris Waterson,HTML 重新排列注意事項
    6. Chris Waterson,Gecko 總覽
    7. Alexander Larsson,HTML HTTP 要求的生命週期
  4. WebKit

    1. David Hyatt,Implementing CSS(part 1)
    2. David Hyatt,WebCore 總覽
    3. David Hyatt,WebCore 轉譯
    4. David Hyatt,FOUC 問題
  5. W3C 規格

    1. HTML 4.01 規格
    2. W3C HTML5 規格
    3. 階層式樣式表第 2 級修訂版本 1 (CSS 2.1) 規格
  6. 瀏覽器建構操作說明

    1. Firefox。https://developer.mozilla.org/Build_Documentation
    2. WebKit。http://webkit.org/building/build.html

翻譯

這個網頁已翻譯成日文兩次:

您可以查看外部託管的韓文和土耳其文翻譯。

感謝大家!