瀏覽器的運作方式

現代網路瀏覽器的幕後

Tali Garsiel
Tali Garsiel

前言

以色列開發人員 Tali Garsiel 的研究成果便是提供 WebKit 和 Gecko 內部運作的全方位入門課程。在過去幾年,她查看了所有已發布的瀏覽器內部資料,並花費大量時間閱讀網路瀏覽器原始碼。她寫道:

身為網頁開發人員,瞭解瀏覽器作業的內部運作方式有助於您做出更明智的決策,並瞭解開發最佳做法的背後原因。雖然這份文件相當冗長,但我們建議您花點時間深入瞭解。很高興您確實如此。

Chrome 開發人員關係團隊成員 Paul Irish

簡介

網頁瀏覽器是最廣泛使用的軟體。在本教學課程中,我將說明這些功能的幕後運作方式。您會看到在網址列中輸入 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. 瀏覽器引擎:將使用者介面與轉譯引擎之間的動作串接。
  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 使用「layout」一詞來放置元素,Gecko 則將其稱為「Reflow」。「Attachment」是 WebKit 的術語,用於連結 DOM 節點和視覺資訊,以建立轉譯樹狀結構。非語意上的小差異在於 Gecko 在 HTML 和 DOM 樹狀結構之間有多餘的圖層。它稱為「內容接收器」,是製作 DOM 元素的工廠。我們將討論流程的各個部分:

剖析 - 一般

由於剖析是轉譯引擎中非常重要的程序,我們將進一步探討這個程序。首先,我們來簡單介紹剖析。

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

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

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

文法

剖析作業會根據文件遵循的語法規則,也就是文件所使用的語言或格式。您可以剖析的每個格式都必須具備確定性的文法,其中包含詞彙和語法規則。這稱為無關聯文法。人類語言並非這類語言,因此無法使用傳統剖析技術剖析。

剖析器 - 字元檢查器組合

解析可分為兩個子程序:字彙分析和語法分析。

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

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

剖析器通常會將工作分派給兩個元件:負責將輸入內容分割成有效符記的剖析器 (有時稱為「剖析器」),以及負責根據語言語法規則分析文件結構,進而建構剖析樹的剖析器

Lexer 知道如何去除不相關的字元,例如空格和換行符號。

從來源文件到剖析樹
圖 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 提供 XHTML 的 XML 變化版本,兩者之間的主要差異是什麼?

差異在於 HTML 方法更「寬容」:您可以省略特定標記 (系統會隱含新增),或有時省略起始或結尾標記等等。整體而言,這是一種「軟」語法,與 XML 的複雜語法不同。

這個看似微不足道的細節,其實意義重大。具體來說,這是 HTML 如此廣受歡迎的主因,可以避免您的錯誤,讓網頁作者的生活更輕鬆。 另一方面,撰寫正式的文法並不容易。總而言之,由於 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 樹狀結構:

範例標記的 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 字元會導致系統建立「開始標記符記」,狀態會變更為「標記名稱狀態」。我們會保留這個狀態,直到用完 > 字元為止。每個字元都會附加至新的符記名稱。在本範例中,建立的權杖是 html 權杖。

到達 > 標記時,系統會發出目前的符記,並將狀態變更回「Data state」。系統會將 <body> 標記以相同步驟處理。到目前為止,系統已傳送 htmlbody 標記。我們現在回到「資料狀態」。使用 Hello worldH 字元會導致字元符記的建立和發出,直到達到 </body>< 為止。我們會為 Hello world 的每個字元產生字元符記。

我們現在回到「代碼開啟狀態」。使用下一個輸入 / 會導致 end tag token 建立,並移至「Tag name state」。我們會一直處於這個狀態,直到達到 >。接著,系統會發出新的代碼權杖,然後我們會回到「資料狀態」</html> 輸入內容會與前述情況相同。

將輸入範例權杖化
圖 10:將範例輸入內容切割為符記

樹狀結構建構演算法

建立剖析器時,系統會建立 Document 物件。在樹狀結構建構階段,系統會修改根目錄中的 DOM 樹狀結構,並新增元素。樹狀結構建構函式會處理分詞器產生的每個節點。每個符記都會定義與這個符記相關的 DOM 元素,並由此權杖建立。這個元素會新增至 DOM 樹狀結構和開啟元素的堆疊中。這個堆疊用於修正巢狀不相符和未關閉的標記。這個演算法也稱為狀態機器。這些狀態稱為「插入模式」。

我們來看看樹狀結構的建立程序,以範例輸入內容為例:

<html>
  <body>
    Hello world
  </body>
</html>

樹狀結構建構階段的輸入內容,是符記化階段的符記序列。第一個模式是「初始模式」。收到「html」權杖後,系統會移至「before html」模式,並在該模式中重新處理權杖。這會導致 HTMLHtmlElement 元素建立作業,並附加至根 Document 物件。

狀態會變更為 "before head"。接著會收到「body」權杖。雖然我們沒有「head」符記,但系統會隱含建立 HTMLHeadElement,並將其新增至樹狀結構。

我們現在要轉移至「in head」模式,然後再轉移至「after head」模式。系統會重新處理主體符記、建立及插入 HTMLBodyElement,並將模式轉移至「in body」

現已接收「Hello World」字串的字元符記。第一個會導致「Text」節點的建立和插入作業,而其他字元會附加到該節點。

接收主體結束權杖後,系統會轉移至「主體結束後」模式。我們現在會收到 HTML 結束標記,並切換至 "after after body" 模式。收到檔案結束符號後,剖析作業就會結束。

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

剖析完成後的動作

在此階段,瀏覽器會將文件標示為互動式,並開始剖析處於「延遲」模式的指令碼,也就是應在剖析文件後執行的指令碼。接著,系統會將文件狀態設為「complete」,並觸發「load」事件。

您可以參閱 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 或內文結束標記錯置

再次強調,評論說得不夠好。

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.errora.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。
圖 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 程式碼會根據顯示屬性,決定應為 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 樹狀結構。「Viewport」是初始包含區塊。在 WebKit 中,這個物件會是「RenderView」物件

樹狀結構的建構流程

在 Firefox 中,系統會將簡報註冊為 DOM 更新的事件監聽器。簡報會將影格建立作業委派給 FrameConstructor,而建構函式會解析樣式 (請參閱「樣式運算」) 並建立影格。

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

處理 HTML 和內文標記會導致建構算繪樹狀結構的根。根轉譯物件對應至 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。我們現在的工作量會減少。

讓我們來看看樹狀結構如何幫助我們節省工作量。

深入研究結構

樣式內容會分成結構體。這些結構體包含邊框或顏色等特定類別的樣式資訊。結構體中的所有屬性都會繼承或不繼承。繼承的屬性 (除非由元素定義) 是沿用自父項的屬性。如未定義非繼承屬性 (稱為「重設」屬性),則會使用預設值。

樹狀結構可在樹狀結構中快取整個結構體 (包含已計算的結束值),協助我們處理這項問題。這個概念是,如果底層節點未提供結構體定義,則可使用上層節點中的快取結構體。

使用規則樹狀結構計算樣式背景資訊

計算特定元素的樣式內容時,我們會先在規則樹狀結構中計算路徑,或使用現有的路徑。接下來,我們會開始套用路徑中的規則,在新的樣式內容中填入結構。我們從路徑的底層節點開始,也就是優先順序最高的節點 (通常是最具體的 selector),然後向上逐一遍歷樹狀結構,直到結構填滿為止。如果該規則節點中沒有結構體規格,我們可以大幅改善效能 - 我們會向上尋找節點,直到找到完整指定該結構體的節點並指向該節點 - 這是最佳的最佳化方式 - 整個結構體都會共用。這樣可以節省結束值和記憶體的計算。

如果我們發現部分定義,就會往樹狀結構上下一層,直到結構填滿為止。

如果我們找不到結構體的任何定義,如果該結構體是「繼承」類型,我們會在內容樹狀結構中指向父項結構體。在本例中,我們已成功共用結構體。 如果是重設結構體,則會使用預設值。

如果最具體的節點確實會增加值,我們就需要做一些額外的計算,來轉換值並轉換成實際值。 接著,我們會將結果快取到樹狀節點,以便供子節點使用。

如果元素有兄弟或姊妹元素,而這些元素指向相同的樹狀節點,則可以共用整個樣式內容

我們來看看以下範例: 假設我們有以下 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 中找到它,這是指定邊界規則的最高層節點。

我們確實有顏色結構的定義,因此無法使用快取的結構體。由於顏色有一個屬性,因此我們不必在樹狀結構上上方來填入其他屬性。我們會計算結束值 (將字串轉換為 RGB 等),並在這個節點上快取計算的結構體。

處理第二個 <span> 元素的工作更簡單。我們會比對規則,並得出結論,指出它指向規則 G,就像前一個 span 一樣。由於同層元素會指向相同節點,因此我們可以共用整個樣式內容,並只指向前一個 span 的內容。

如果結構體包含從父項繼承的規則,則會在內容樹狀結構中快取 (顏色屬性實際上會繼承,但 Firefox 會將其視為重設,並在規則樹狀結構中快取)。

舉例來說,如果我們針對段落中的字型新增規則:

p {font-family: Verdana; font size: 10px; font-weight: bold}

接著,段落元素 (即內容樹狀結構中 div 的子項) 可能會與其父項共用相同的字型結構。這是指未為段落指定字型規則的情況。

在沒有規則樹狀結構的 WebKit 中,系統會四次遍歷符合條件的宣告。系統會套用第一個不重要的高優先順序屬性 (某些屬性需先套用屬性,例如多媒體),然後高優先順序、一般優先順序不重要,再套用一般優先順序重要規則。 也就是說,如果屬性出現多次,系統將按照正確的串列順序解析。最後一個為準。

總而言之,共用樣式物件 (完全或其中的某些結構體) 可以解決問題 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,其尺寸為可視區域,也就是瀏覽器視窗的可視部分。

所有轉譯器都有「版面配置」或「重排」方法,每個轉譯器都會針對需要版面配置的子項叫用版面配置方法。

髒位元系統

為了避免每次進行小幅變更時都重新排版,瀏覽器會使用「髒位元」系統。變更或新增的轉譯器會將自身和子項標示為「髒」:需要版面配置。

標記有「骯髒」和「子項骯髒」兩種。也就是說,雖然轉譯器本身也可以接受,但至少有一個子項需要版面配置。

全域和逐步版面配置

版面配置可在整個轉譯樹狀結構中觸發 - 這就是「全域」版面配置。可能的原因如下:

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

版面配置可以漸進式,只有骯髒的轉譯器會展開 (這可能會導致某些損壞需要額外的版面配置)。

當轉譯器不一致時,系統會以非同步方式觸發漸進式版面配置。舉例來說,當額外內容從網路傳送並新增至 DOM 樹狀結構後,新轉譯器就會附加至轉譯樹狀結構。

漸進式版面配置。
圖 18:增量版面配置 - 只會排版已變更的轉譯器及其子項

非同步和同步版面配置

增量版面配置會以非同步方式完成。Firefox 會為漸進式版面配置排入「重新流動指令」,而排程器會觸發這些指令的批次執行作業。WebKit 也提供計時器,可執行漸進式版面配置,也就是會逐一檢查樹狀結構,並將「髒」轉譯器排版。

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

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

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

最佳化

當版面配置由「調整大小」觸發,或轉譯器位置(而非大小) 發生變更時,系統會從快取中取得轉譯大小,而非重新計算…

在某些情況下,只有子樹狀結構會遭到修改,版面配置則並非從根目錄開始。當變更是局部且不會影響周圍內容時,就可能發生這種情況,例如插入文字欄位的文字 (否則每個按鍵都會觸發從根目錄開始的版面配置)。

版面配置程序

版面配置通常具有以下模式:

  1. 上層轉譯器會確定自己的寬度。
  2. 父項會檢查子項,並執行下列操作:
    1. 放置子項轉譯器 (設定其 x 和 y)。
    2. 視需要呼叫子版面配置 (如果子版面配置不正確或處於全域版面配置,或其他原因),以便計算子項的高度。
  3. 父項會使用子項的累積高度,以及邊框和邊框間距的高度,設定自己的高度。這會由父項轉譯器的父項使用。
  4. 將骯髒位元設為 false。

Firefox 使用「狀態」物件(nsHTMLReflowState) 做為版面配置的參數 (結尾為「reflow」)。其他狀態包含父項寬度。

Firefox 版面配置的輸出內容是「metrics」物件(nsHTMLReflowMetrics)。其中包含轉譯器計算出的高度。

寬度計算

算出轉譯器的寬度時,系統會使用容器區塊的寬度、轉譯器的樣式「width」屬性、邊界和邊框。

例如,下列 div 的寬度:

<div style="width: 30%"/>

WebKit 會計算以下值(RenderBox 類別的 calcWidth 方法):

  • 容器寬度為容器可用的寬度和 0 的最大值。在這種情況下,availableWidth 就是 contentWidth,計算方式如下:
clientWidth() - paddingLeft() - paddingRight()

clientWidth 和 clientHeight 代表物件的內部,不含邊框和捲軸。

  • 元素的寬度是「width」樣式屬性。計算容器寬度的百分比做為絕對值。

  • 水平邊框和邊距現已加入。

這就是「偏好寬度」的計算方式。系統現在會計算最小和最大寬度。

如果偏好的寬度大於寬度上限,則會使用寬度上限。如果寬度小於最小寬度 (無法中斷的最小單位),則會使用最小寬度。

如果需要版面配置,這些值將會快取,但寬度不會改變。

換行

當版面配置中間的轉譯器判定轉譯器需要中斷時,轉譯器就會停止,並套用到需要損毀的版面配置父項。父項會建立額外的轉譯器,並在這些轉譯器上呼叫版面配置。

繪畫

在繪製階段,系統會逐一檢查轉譯樹狀結構,並呼叫轉譯器的「paint()」方法,在螢幕上顯示內容。繪製功能會使用 UI 基礎架構元件。

全域和增量

跟版面配置一樣,繪畫也是全域通用的,一種是塗上或漸進式的樹木。在逐步繪製時,部分轉譯器會以不會影響整個樹狀結構的方式進行變更。變更後的轉譯器會使其在螢幕上的矩形失效。這會導致作業系統將其視為「髒區域」,並產生「繪圖」事件。作業系統會巧妙地將多個區域合併為一個區域。Chrome 會比較複雜,因為轉譯器和主要程序不同,Chrome 會在某種程度上模擬作業系統的行為。呈現方式會監聽這些事件,並將訊息委派給轉譯根。樹狀結構會週遊,直到到達相關的轉譯器。它會重新繪製自身 (通常是子項)。

繪製順序

CSS2 定義了繪製程序的順序。這實際上是元素在堆疊結構定義中的堆疊順序。這種順序會影響繪製,因為堆疊是從後到前方繪製。區塊轉譯器的堆疊順序如下:

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

Firefox 顯示清單

Firefox 會查看轉譯樹狀結構,並為已著色的矩形建立顯示清單。其中包含與矩形相關的轉譯器,並採用正確的繪製順序 (包括轉譯器的背景、邊框等)。

這樣一來,系統只需為重新繪製作業遍歷一次樹狀結構,而非多次遍歷 (繪製所有背景、所有圖片、所有邊框等)。

Firefox 會避免加入會遭到隱藏的元素,例如完全位於其他不透明元素下方的元素,以便最佳化這個程序。

WebKit 矩形儲存空間

重新繪製前,WebKit 會將舊矩形儲存為位圖。接著,只會繪製新矩形與舊矩形之間的差異。

動態變更

瀏覽器會盡可能減少可能造成的影響,以因應變更。因此,變更元素的顏色會導致只會重新繪製元素。變更元素位置會導致元素、子項和可能的同胞元素重新繪製版面配置。新增 DOM 節點會導致節點的版面配置和重繪。重大變更 (例如增加「html」元素的字型大小) 會導致快取無效,並重新排版及重新繪製整個樹狀結構。

轉譯引擎的執行緒

算繪引擎為單執行緒。除了網路作業,幾乎所有作業都會在單一執行緒中執行。在 Firefox 和 Safari 中,這是瀏覽器的主執行緒。Chrome 則是分頁處理主執行緒。

網路作業可由多個並行執行緒執行。並行連線數量有限 (通常為 2 到 6 個連線)。

事件迴圈

瀏覽器主執行緒是事件迴圈。這是一個無限迴圈,可讓程序持續運作。會等待事件 (例如版面配置和繪圖事件),並加以處理。以下是主要事件迴圈的 Firefox 程式碼:

while (!mExiting)
    NS_ProcessNextEvent(thread);

CSS2 視覺模型

畫布

根據 CSS2 規格,「Canvas」一詞是指「格式化結構的轉譯空間」一詞,是瀏覽器繪製內容的位置。

畫布的每一個尺寸都沒有限制,但瀏覽器會根據可視區域的尺寸選擇初始寬度。

根據 www.w3.org/TR/CSS2/zindex.html,如果畫布包含在另一個畫布中,則會是透明的,如果不包含,則會指定瀏覽器定義的顏色。

CSS Box 型號

CSS 盒模型會說明為文件樹狀結構中的元素產生矩形方塊,並根據視覺格式設定模型進行版面配置。

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

CSS2 Box 型號
圖 19:CSS2 方塊模型

每個節點都會產生 0…n 個這樣的方塊。

所有元素都有「顯示」屬性,用來決定要產生的方塊類型。

範例:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

預設值為內嵌,但瀏覽器樣式表可能會設定其他預設值。舉例來說,「div」元素的預設顯示方式為區塊。

如需預設樣式表範例,請前往 www.w3.org/TR/CSS2/sample.html

定位配置

目前共有三種配置:

  1. 一般:物件會根據在文件中的位置決定。也就是說,它在轉譯樹狀結構中的位置,就像在 DOM 樹狀結構中的位置一樣,並根據其方塊類型和尺寸進行版面配置
  2. 浮動:物件會先像正常流程一樣排版,然後盡可能向左或右移動
  3. 絕對:物件會放置在轉譯樹狀結構中,而非 DOM 樹狀結構

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

  • 靜態和相對狀況導致正常流程
  • 絕對和固定會導致絕對定位

在靜態定位中,系統不會定義位置,而是使用預設定位。在其他配置中,作者會指定位置:上、下、左、右。

盒子的配置方式取決於:

  • 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, Compilers: Principles, Techniques, and Tools (又稱「Dragon book」),Addison-Wesley,1986 年
    2. Rick Jelliffe。The Bold and the Beautiful:兩個新的 HTML5 草稿。
  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 tech talk video)
    3. L. David Baron,Mozilla 版面配置引擎
    4. L. David Baron,Mozilla 系統說明文件樣式
    5. Chris Waterson,HTML Reflow 注意事項
    6. Chris Waterson,Gecko 總覽
    7. Alexander Larsson,HTML HTTP 要求的生命週期
  4. WebKit

    1. David Hyatt,CSS 實作(第 1 部分)
    2. David Hyatt, An Overview of WebCore
    3. David Hyatt,WebCore 算繪
    4. FOUC 問題 David Hyatt
  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

翻譯

本頁面已翻譯成日文,兩次:

您可以檢視韓文土耳其文的外部託管翻譯。

謝謝大家!