瀏覽器的運作方式

新式網路瀏覽器幕後花絮

Paul Irish
Tali Garsiel
Tali Garsiel

前言

透過這項全面瞭解 WebKit 和 Gecko 內部營運的基本概念,也是以色列開發人員 Tali Garsiel 進行大量研究的結果。多年來,她回顧了所有發布的瀏覽器內部相關資料,花了很多時間閱讀網路瀏覽器原始碼。她寫道:

網頁開發人員可以瞭解瀏覽器作業的內部元件,幫助您做出更好的決策,並瞭解開發最佳做法背後的理由。雖然這份文件內容相當冗長,但仍建議您花點時間瞭解。您會很高興您進行過變更。

Chrome 開發人員關係部門 Paul Ireland

引言

網路瀏覽器是使用率最高的軟體。在這個入門中,我將說明 這些功能的幕後運作原理您在網址列中輸入 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 文件,並將元素轉換為樹狀結構中的「內容樹狀結構」節點。引擎會剖析外部 CSS 檔案和樣式元素中的樣式資料。使用 HTML 中的樣式資訊搭配視覺化操作說明來建立另一個樹狀結構:轉譯樹狀結構

轉譯樹狀結構中包含具有顏色和維度等視覺屬性的矩形。矩形的顯示順序正確無誤,因此會顯示在螢幕上。

轉譯樹狀結構建構完成後,會經過「版面配置」程序。也就是說,請為各個節點提供在畫面中應顯示的確切座標。下一階段是「paint」(繪製) - 系統會掃遍轉譯樹狀結構,並使用 UI 後端層繪製每個節點。

請務必瞭解這只是逐步處理的過程。為提供更優質的使用者體驗,算繪引擎會盡快在螢幕上顯示內容。而是要等到所有 HTML 剖析完畢之後,才能開始建構及版面配置轉譯樹狀結構。系統會剖析及顯示內容的部分內容,而這項程序會繼續來自網路的其餘內容。

主要流程範例

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

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

Gecko 將視覺格式元素的樹狀結構稱為「框架樹狀結構」。每個元素都是一個影格。WebKit 使用「算繪樹狀結構」一詞,當中包含「算繪物件」。WebKit 使用「版面配置」一詞來放置元素,Gecko 則稱之為「Reflow」。「Attachment」是 WebKit 用來連結 DOM 節點和視覺資訊來建立轉譯樹狀結構的術語。細微的非語意差異在於 Gecko 在 HTML 和 DOM 樹狀結構之間有一個額外圖層。它稱為「內容接收器」,是建立 DOM 元素的工廠。 我們會說明流程的各個部分:

剖析 - 一般

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

剖析文件的意思是,將其轉換成程式碼可以使用的結構。剖析結果通常是節點樹狀結構,代表文件的結構。這就是所謂的剖析樹狀結構或語法樹狀結構。

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

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

文法

剖析作業是根據文件遵循的語法規則:撰寫語言或格式。所有可剖析的格式都必須具有確定性的文法,其中包含詞彙和語法規則。這稱為「沒有語境的文法」。人類語言不是這種語言,因此無法透過傳統剖析技術剖析。

剖析器 - Lexer 組合

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

詞法分析是將輸入內容拆分為代碼的過程。符記是語言詞彙,即有效構成要素的集合。在人類語言中,這會包含該語言字典中出現的所有字詞。

語法分析負責套用語言語法規則。

剖析器通常會將工作分為兩個元件:負責將輸入拆分為有效符記的 lexer (有時稱為「符記化工具」),以及會根據語言語法規則分析文件結構,負責建構剖析樹狀結構的「剖析器」

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

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

剖析程序是疊代的過程。剖析器通常會要求 lexer 提供新的權杖,並嘗試將權杖與其中一個語法規則進行比對。如果規則比對相符,系統就會將與該符記對應的節點加入剖析樹狀結構,而剖析器會要求另一個符記。

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

翻譯

在許多情況下,剖析樹狀結構並非最終成品。剖析功能經常用於翻譯,也就是將輸入文件轉換成其他格式。例如編譯。將原始碼編譯為機器程式碼的編譯器,會先將其剖析為剖析樹狀結構,然後再將樹狀結構轉譯成機器程式碼文件。

編譯流程
圖 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 個
term + 3 - 1 個
字詞運算 3 至 1 個
運算式 - 1 個
運算式運算 1
運算式 -

這種由下而上剖析器稱為 shift-縮小剖析器,因為輸入內容會移到右側 (想像在輸入開始時先指向右側指標),並逐漸降低為語法規則。

自動產生剖析器

有些工具可產生剖析器。輸入您語言的文法,包括詞彙和語法規則,然後就能產生運作剖析器。建立剖析器需要深入瞭解剖析,而且手動建立最佳化剖析器並不容易,因此剖析器產生器可派上用場。

WebKit 使用兩種知名的剖析器產生器,分別是 Flex 建立 lexer,以及 Bison 來建立剖析器 (您可以使用 Lex 和 Yacc 名稱來執行這些剖析器)。 彈性輸入是含有符記規則運算式定義的檔案。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 樹狀結構:

範例標記的 DOM 樹狀結構
圖 8:範例標記的 DOM 樹狀結構

DOM 與 HTML 一樣,是由 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>

初始狀態為「資料狀態」。遇到 < 字元時,狀態會變更為「代碼開放狀態」。使用 a-z 字元會建立「開始標記符記」,狀態會變更為「代碼名稱狀態」。除非耗用 > 字元,否則我們會保持在這個狀態。每個字元都會附加至新的權杖名稱。在本範例中,建立的權杖為 html 權杖。

假如觸及的是 > 標記,系統會發出目前的權杖,並將狀態改回「資料狀態」。系統會按照相同步驟處理 <body> 標記。目前已發出 htmlbody 標記。現在可以回到「資料狀態」。只要使用 Hello worldH 字元,系統就會建立並發出字元符記,這項作業會一直執行,直到達到 </body>< 為止。我們會針對 Hello world 的每個字元發出字元權杖。

您現在可以回到「代碼開啟狀態」。 如果使用下一個輸入項目 /,系統就會建立 end tag token,並移至「標記名稱狀態」。我們要再次保持在這個狀態,直到到達 > 為止。然後,系統會發出新的標記權杖,並返回「資料狀態」。系統會將 </html> 輸入內容視為上一個案例。

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

樹木建構演算法

建立剖析器時,會建立文件物件。在樹狀結構建構階段,系統會修改內含文件根文件的 DOM 樹狀結構,並加入元素。權杖化工具發出的每個節點都會由樹狀結構建構函式處理。每個權杖的規格都會定義哪些 DOM 元素與該權杖相關,並且將為這個權杖建立。該元素會新增至 DOM 樹狀結構以及開啟元素的堆疊中。這個堆疊可以修正巢狀結構不相符和未結束的標記。演算法亦稱為狀態機器。狀態稱為「插入模式」。

以下是輸入範例的樹狀結構建構程序:

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

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

狀態將會變更為 "before head"。然後接收「body」符記。HTMLHeadElement 會以隱含方式建立,雖然我們沒有「head」符記,但會新增到樹狀結構中。

現在請切換至「在頭部」模式,然後移至「在頭後」。主體符記經過重新處理後,會建立並插入 HTMLBodyElement,模式也會傳輸至「內文」中。

現已接收「Hello World」字串的字元權杖。第一個指令會建立並插入「文字」節點,其他字元則會附加到該節點。

接收主體結束權杖之後,系統就會轉移至 "在主體之後" 模式。我們現在會收到 html 結束標記,這組代碼會將我們移至「<內文後>」模式。接收檔案權杖結尾會終止剖析。

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 規格說明),其中包含寬度、高度和位置等幾何圖形資訊。

方塊類型會受到節點相關樣式屬性的「顯示」值影響 (請參閱「樣式計算」一節)。以下 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」的元素也不會顯示在樹狀結構中,但顯示設定為「隱藏」的元素則會顯示在樹狀結構中。

有些 DOM 元素會對應好幾個視覺物件。通常具有複雜結構的元素,無法用單一矩形描述。舉例來說,「select」元素有三個轉譯器,一個用於顯示區域,一個用於下拉式清單方塊,另一個用於按鈕。另外,如果因一行的寬度不足以容納一行文字,系統將文字分成多行時,新行會加入為額外的轉譯器。

多個轉譯器的另一個例子是 HTML 毀損。根據 CSS 規格,內嵌元素只能包含區塊元素或僅內嵌元素。 如果是混合內容,系統會建立匿名封鎖轉譯器,納入內嵌元素。

部分算繪物件會與 DOM 節點對應,但並非位於樹狀結構中的不同位置。浮動和絕對定位元素移出流程、會放置在樹狀結構的不同部分,然後對應到真實的影格。就應有預留位置頁框。

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

建構樹狀結構的流程

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

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

處理 HTML 和內文標記會產生算繪樹狀結構的根層級。根轉譯物件與 CSS 規格呼叫包含區塊的對應項目,也就是包含所有其他區塊的最上層區塊。其尺寸即為可視區域:瀏覽器視窗顯示區域維度。Firefox 將其稱為 ViewPortFrame,而 WebKit 將其稱為 RenderView。這是文件指向的轉譯物件。樹狀結構的其餘部分是以 DOM 節點插入的方式建構。

請參閱處理模式的 CSS2 規格

樣式計算

建立轉譯樹狀結構需要計算每個轉譯物件的視覺屬性。方法是計算每個元素的樣式屬性。

這個樣式包含 HTML 中不同來源的樣式表、內嵌樣式元素和視覺屬性 (例如「bgcolor」屬性)。之後會轉譯為相符的 CSS 樣式屬性。

樣式表的來源是瀏覽器的預設樣式表,也就是網頁作者和使用者樣式表提供的樣式表,這些都可以是瀏覽器使用者提供的樣式表 (瀏覽器可讓您定義喜愛的樣式)。例如,在 Firefox 中,您只要將樣式表放入「Firefox 設定檔」資料夾即可。

樣式計算會帶來一些困難:

  1. 樣式資料是一個非常大型的結構,擁有眾多樣式屬性,可能會造成記憶體問題。
  2. 如果未將元素最佳化,則尋找每個元素的比對規則可能會導致效能問題。逐一瀏覽每個元素的完整規則清單來找出相符項目是相當繁瑣的工作。選取器的結構可能很複雜,導致比對程序開始時,採用經證實為模糊不清的路徑,而另一個必須嘗試的路徑。

    例如 - 這個複合選取器:

    div div div div{
    ...
    }
    

    也就是套用到規則的 <div>,也就是 3 個 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 的定義,那麼如果結構體屬於「繼承」類型,我們會指向結構定義樹狀結構中父項的結構。在本案例中,我們也成功地共用結構。 如果是重設結構,系統就會使用預設值。

如果最具體的節點確實會新增值,我們就需要做一些額外的運算,才能將其轉換為實際值。我們會在樹狀結構節點中快取結果,以供子項使用。

如果元素的同層級或子母之間指向同一個樹狀結構節點,則可在兩者之間共用整個樣式結構定義

讓我們看看範例: 假設我們有

<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,就像先前的時距一樣。由於我們有指向同一個節點的同層級,因此可以共用整個樣式背景資訊,並僅指向前一個跨距的情境。

如果結構結構包含沿用自父項的規則,系統會在結構定義樹狀結構上執行快取 (實際上顏色屬性是沿用,但 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 屬性來對應 HTML 屬性。

如先前問題 #2 所述,CSS 規則比對可能較為複雜。 為解決這個問題,我們調整規則以方便存取。

剖析樣式表後,系統會根據選取器將規則新增至數種雜湊對應中的其中一種。其中包括按照 ID、類別名稱、按標記名稱分類的地圖,以及不符合這些類別的任何項目的一般地圖。如果 selector 是 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 使用「state」物件(nsHTMLReflowState) 做為版面配置的參數 (終止的「重排」)。狀態包括父項寬度。

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

寬度計算

轉譯器寬度是根據容器區塊的寬度、轉譯器樣式的「寬度」屬性、邊界和框線計算而得。

例如下列 div 的寬度:

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

WebKit 的計算方式如下(RenderBox 方法 calcWidth):

  • 容器寬度是可用容器寬度與 0 的最大值。在此情況下,AvailableWidth 是 contentWidth,計算方式為:
clientWidth() - paddingLeft() - paddingRight()

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

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

  • 現已新增水平框線和邊框間距。

到目前為止,這是計算「偏好寬度」的值。 現在系統會計算寬度的最小和最大寬度。

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

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

換行

如果版面配置中間的轉譯器決定需要破壞,轉譯器就會停止,並套用至版面配置的父項,導致轉譯器需要毀損。父項會建立額外的轉譯器並呼叫版面配置。

繪畫

在繪圖階段中,轉譯樹狀結構經過掃遍,系統會呼叫轉譯器的「paint()」方法,在螢幕上顯示內容。 繪製作業會使用 UI 基礎架構元件。

全域增量和增量

和版面配置一樣,繪圖也可以是全球性的,也就是繪製整棵樹,甚至是漸增。在漸進式繪圖中,部分轉譯器的變更方式不會影響整個樹狀結構。變更後的轉譯器會使畫面上的矩形失效。這會導致 OS 將其視為「無效區域」,並產生「繪製」事件。OS 會巧妙地將數個區域合併為一個區域。Chrome 的轉譯程序比較複雜,因為轉譯器和主要程序不同。Chrome 會以某種程度來模擬作業系統行為。 簡報會監聽這些事件,並將訊息委派給轉譯根目錄。系統會掃遍樹狀結構,直到到達相關的轉譯器為止。它會自行繪製 (通常是子項)。

繪製順序

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 Box 型號

「CSS 方塊模型」說明瞭為文件樹狀結構中的元素產生的矩形方塊,並根據視覺化格式模型配置這些方塊。

每個方塊都有內容區域 (例如文字、圖片等),但四周的邊框間距、框線和邊界區域可視需要選用。

CSS2 盒子型號
圖 19:CSS2 方塊型號

每個節點會產生 0 個此類方塊。

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

例:

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」屬性設定。

  • 正常資料流
  • 絕對和固定原因

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

盒子的排列方式取決於:

  • 方塊類型
  • 方塊尺寸
  • 定位配置
  • 外部資訊,如圖片大小和螢幕大小

方塊類型

方塊:形成一個區塊 - 在瀏覽器視窗中具有專屬的矩形。

封鎖方塊。
圖 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,艾倫。網路瀏覽器參考架構 (pdf)
    2. Gupta、Vineet。瀏覽器的運作方式 - 第 1 部分 - 架構
  2. 剖析

    1. Aho、Sethi、Ullman、Compilers:原則、技術與工具 (又稱「龍書」),Addison-Wesley,1986 年
    2. Rick Jelliffe。吸睛的《The Bold and the Beautiful》:HTML 5 有兩款新草稿。
  3. Firefox

    1. L. David Baron,更快的 HTML 和 CSS:Layout Engine 內部適合網頁開發人員
    2. L. David Baron,更快的 HTML 和 CSS:Layout Engine 內部供網頁開發人員使用 (Google 技術講座影片)
    3. L. Mozilla 的版面配置引擎 David Baron
    4. L. Mozilla Style System 說明文件 David Baron
    5. Chris Waterson,HTML 重排注意事項
    6. Chris Waterson,Gecko 總覽
    7. Alexander Larsson,HTML HTTP 要求的生命週期
  4. WebKit

    1. David Hyatt,導入 CSS(第 1 部分)
    2. WebCore 總覽 David Hyatt
    3. WebCore 轉譯的 David Hyatt
    4. FOUC 問題 David Hyatt
  5. W3C 規格

    1. HTML 4.01 規格
    2. W3C HTML5 規格
    3. Cascading StyleSheet Level 2 第 2 級修訂版本 1 (CSS 2.1) 規格
  6. 瀏覽器建構操作說明

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

翻譯

系統已將這個網頁翻譯成日文,並翻譯成日文兩次:

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

謝謝大家!