瀏覽器的運作方式

新式網路瀏覽器的幕後花絮

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 文件,並將元素轉換為樹狀結構中的「內容樹狀結構」DOMDOM 節點。引擎會剖析外部 CSS 檔案和樣式元素中的樣式資料。設定資訊樣式和 HTML 中的視覺指示時,將會用來建立另一個樹狀圖,也就是「轉譯樹狀結構」

算繪樹狀結構包含帶有色彩和尺寸等視覺屬性的矩形。 矩形在畫面中顯示的順序正確無誤。

算繪樹狀結構建構完成後,會經過「版面配置」上傳資料集之後,您可以運用 AutoML 自動完成部分資料準備工作 也就是為每個節點提供其在螢幕上顯示的確切座標。 下一階段是繪製 - 會掃遍轉譯樹狀結構,並使用 UI 後端層繪製各個節點。

請務必瞭解,這項程序會逐步進行。為提供更好的使用者體驗,轉譯引擎會盡快在畫面上嘗試顯示內容。 等到所有 HTML 都剖析完畢之後,才能開始建構及配置轉譯樹狀結構。 系統將剖析並顯示部分內容,並在處理程序中繼續執行其他來自網路的內容。

主要流程範例

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

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

Gecko 將視覺格式元素的樹狀結構稱為「影格樹狀結構」。每個元素都是一個影格。 WebKit 使用「Render Tree」一詞以及「算繪物件」 WebKit 使用「layout」一詞放置元素,Gecko 將其稱為「Reflow」。 「附件」是 WebKit 用來連結 DOM 節點,以及建立算繪樹狀結構的視覺化資訊。 非語意上的小差異在於 Gecko 在 HTML 和 DOM 樹狀結構之間有多餘的圖層。稱為「內容接收器」是製作 DOM 元素的工廠 我們將探討流程的各個部分:

剖析 - 一般

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

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

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

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

文法

這項剖析作業是以文件所使用的語法規則為依據,也就是寫入的語言或格式。 每個能剖析的格式,都必須具有確定性文法,包括詞彙和語法規則。這稱為 沒有背景資訊的文法。人類語言不是這種語言,因此無法使用傳統剖析技術進行剖析。

剖析器 - Lexer 組合

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

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

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

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

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

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

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

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

翻譯

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

編譯流程
圖 7:編譯流程

剖析範例

在圖 5 中,我們透過數學運算式建立了剖析樹狀結構。 我們來嘗試定義簡單的數學語言,看看剖析過程。

語法:

  1. 語言語法建構模塊包括運算式、字詞和作業。
  2. 我們的語言可包含不限數量的表情。
  3. 運算式定義為「term」後面接著「運算」後面接著另一個字詞
  4. 運算是指加號或減號符記
  5. 字詞是整數符記或運算式

現在來分析輸入 2 + 3 - 1

根據第 5 條規則,第一個與規則相符的子字串是 2。 第二個比對相符是 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
運算式運算 1
運算式 -

這類「由下而上」剖析器稱為「shift-縮減」剖析器,因為輸入內容會移至右側 (假設從輸入開始處指向右側指向右方的指標),並逐漸縮減為語法規則。

自動產生剖析器

有些工具可以產生剖析器,您只要提供語言的文法,包括單詞和語法規則,然後即可產生可正常運作的剖析器。 要建立剖析器需要深入瞭解剖析,而且要手動建立最佳化的剖析器並不容易,因此剖析器產生器非常實用。

WebKit 使用兩種廣為人知的剖析器產生器:Flex 用來建立 Lexer,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 樹狀結構

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:為範例輸入內容權杖化

樹木建設演算法

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

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

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

樹狀結構建構階段的輸入內容是代碼化階段的一連串符記。 第一個模式是「初始模式」,正在接收「html」權杖會移至 「在 html 之前」模式,在該模式下重新處理權杖。 這麼做會建立 HTMLHtmlElement 元素,並附加至根 Document 物件。

狀態將變更為「before head"」。「內文」符記雖然我們沒有「head」權杖,這個權杖會新增至樹狀結構中。

我們現在會移至「in head」模式,然後再移至「 after head」。系統會重新處理 body 權杖,系統會建立並插入 HTMLBodyElement,並將模式轉移至 "in body"

「Hello World」的字元符記字串。第一項會建立和插入「文字」其他字元就會附加至該節點。

接收主體結束權杖後,系統會將資料轉移至 「after body」模式。 我們現在會收到 HTML 結束標記,此標記會將我們移至「在主體之後」模式。 收到檔案權杖結尾後,剖析就會結束。

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

剖析完成時的動作

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

您可以看到 HTML5 規格中有關代碼化和樹狀結構建構的完整演算法

瀏覽器的錯誤容忍度

系統絕不會顯示「語法無效」錯誤。 瀏覽器會修正所有無效的內容,然後繼續推出遊戲。

以這段 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}*

&quot;ident&quot;是 ID 的簡稱,例如類別名稱 「名稱」是一個元素 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 代表空格)。 規則集包含大括號,中間的宣告或選用多個以半形分號分隔的宣告。 「聲明」和「selector」會定義在下列 BNF 定義中

WebKit CSS 剖析器

WebKit 使用 Flex 與 Bison 剖析器產生器,自動從 CSS 文法檔案建立剖析器。 您在剖析器簡介時想回想一下,Bison 建立了由下而上移位 - 減少剖析器的剖析器。 Firefox 使用手動編寫的「上半部」剖析器。 在這兩種情況下,每個 CSS 檔案都會剖析為 StyleSheet 物件。每個物件都包含 CSS 規則。CSS 規則物件包含選取器和宣告物件,以及與 CSS 文法相對應的其他物件。

剖析 CSS。
圖 12:剖析 CSS

指令碼和樣式表的處理順序

指令碼

網路模型為同步性質,作者會預期在剖析器觸及 <script> 標記時,剖析並執行指令碼。 指令碼執行完畢前,文件剖析作業會暫停。 如果是外部指令碼,則必須先從網路擷取資源 (這項作業也會同步執行),並且會暫停剖析,直到系統擷取資源為止。 這種模型已有多年經驗,有 HTML4 和 5 規格也一併指定。 作者可以加上「延遲」屬性,因此不會停止文件剖析作業,並在文件剖析完成後執行。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 元素的關係。 但不會在算繪樹狀結構中插入非視覺 DOM 元素。例如元素。顯示值已指派給「無」的元素也包含在內,就不會出現在樹狀結構中 (而具有「隱藏」瀏覽權限的元素則會出現在樹狀結構中)。

有些 DOM 元素會對應至多個視覺物件。這些通常是結構複雜的元素,無法由單一矩形說明。例如「選取」元素有三個轉譯器:一個用於顯示區域、一個用於下拉式清單方塊,另一個用於按鈕。 此外,由於寬度不夠長,導致文字無法容納成多行時,系統會將新的行視為額外的轉譯器。

另一個多個轉譯器的例子是 HTML 無效。 根據 CSS 規格,內嵌元素只能包含區塊元素或僅限內嵌元素。 如果是混合內容,系統會建立匿名區塊轉譯器,用來包裝內嵌元素。

部分轉譯物件會對應至 DOM 節點,但與樹狀結構中的不同位置不同。 浮點值和絕對位置元素超出流動,置於樹狀結構的不同部分,且會對應到實際的影格。 預留位置影格就是應放置的位置。

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

建構樹狀結構的流程

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

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

處理 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. 元素必須使用相同的滑鼠狀態 (例如,其中一個元素不得處於「滑鼠懸停」狀態,而另一個則不能)
  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。現在要處理的工作減少了。

現在來看看樹狀圖如何幫助我們處理工作。

深入研究結構

樣式情境會分為多個結構。這些結構包含特定類別的樣式資訊,例如框線或顏色。結構中的所有屬性均為繼承或未沿用。繼承的屬性 (除非由元素定義) 是沿用自父項的屬性。如未定義,未沿用的屬性 (稱為「重設」屬性) 會使用預設值。

樹狀結構能協助我們在樹狀結構中快取整個結構 (包含計算出的結束值)。概念:如果底部節點未提供結構體的定義,可使用上方節點中的快取結構體。

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

計算特定元素的樣式情境時,我們會先計算規則樹狀結構中的路徑,或使用現有的路徑。 接下來,我們會開始套用路徑中的規則,在新的樣式內容中填入結構。我們會從路徑的底部節點開始:優先順序最高的選取器 (通常是最明確的選取器),接著在結構體滿前掃遍整個樹狀結構。 如果規則節點中沒有結構體的規格,我們就能大幅改進。我們會先回到樹狀結構,直到找到能完全指定該結構的節點 (亦即最佳的最佳化),也就是共用整個結構。 這樣可以節省結束值和記憶體的計算。

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

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

如果最具體的節點確實會增加值,我們就需要做一些額外的計算,來轉換其為實際值。 然後,我們會將結果快取到樹狀結構節點中,供子項使用。

如果元素包含指向同一個樹狀結構節點的同層或兄弟,則可在兩者之間共用整個樣式的背景脈絡

範例如下: 假設我們準備了這段 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}

為了簡化作業,我們只需填寫兩個結構體:顏色結構和邊界結構。 color struct 僅包含一個成員:顏色 邊界結構包含四個面。

產生的規則樹狀結構看起來會像這樣 (節點會標示節點名稱:指向的規則數量):

規則樹狀結構
圖 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 屬性也可使用元素作為鍵進行對應。

如同先前在問題 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 元素的規則。類別地圖會包含「錯誤」「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 會利用旗標,標示是否已載入所有頂層樣式表 (包括 @import)。 如果附加樣式時沒有完全載入樣式,系統會使用預留位置,並在文件中標示該樣式,並在載入樣式表後重新計算。

版面配置

建立轉譯器並新增至樹狀結構時,系統不會設定轉譯器的位置和大小。計算這些值的行為稱為版面配置或重排。

HTML 採用以流程為基礎的版面配置模型,意即在大部分的情況下,您都可以在單次傳遞中計算幾何圖形。之後「流程中」的元素通常不會影響較早「流程」中元素的幾何圖形,因此在文件版面配置中,可以從左到右、由上到下依序處理文件。例外狀況如下:舉例來說,HTML 表格可能需要多個通行證。

座標系統以根框架為相對。系統會使用頂端和左側座標。

版面配置是週期性程序。始於根轉譯器,與 HTML 文件的 <html> 元素相對應。版面配置會持續以遞迴方式通過部分或所有影格階層,為每個有需要的轉譯器計算幾何圖形資訊。

根轉譯器的位置為 0,0,尺寸則是可視區域,也就是瀏覽器視窗的可見部分。

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

骯髒系統

為避免每次小幅調整都有完整版面配置,瀏覽器會使用「骯髒位元」有些人會將 Cloud Storage 視為檔案系統 但實際上不是 對轉譯器進行變更或新增其本身及其子項為「骯髒」的轉譯器需要版面配置。

一共有兩大旗幟:「骯髒」和「小孩很骯髒」這表示雖然轉譯器本身也可以正常運作,但至少有一個子項需要版面配置。

全域和漸進式版面配置

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

  1. 會影響所有轉譯器的全域樣式變更,例如字型大小變更。
  2. 為獲得最佳畫面大小

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

當轉譯器髒亂時,就會觸發 (非同步) 版面配置。例如,在額外內容來自網路且新增至 DOM 樹狀結構後,系統會將新的轉譯器附加至轉譯樹狀結構。

漸進式版面配置。
圖 18:版面配置漸進式版面配置 - 只配置出骯髒轉譯器及其子項

非同步和同步版面配置

漸進式版面配置會以非同步方式完成。Firefox 將「重排指令」排入佇列,且排程器會觸發這些指令的批次執行作業。 WebKit 還設有計時器,可執行漸進式版面配置 - 樹木會經過週遊和「骯髒」轉譯器會配置版面配置

要求提供樣式資訊的指令碼,例如「offsetHeight」能以同步方式觸發漸進式版面配置

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

有時候,版面配置會在初始版面配置後以回呼的形式觸發,這是因為部分屬性 (例如捲動位置) 發生變化。

最佳化

「調整大小」觸發版面配置時或變更轉譯器位置(而非大小) 時,轉譯大小會從快取取得,不會重新計算...

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

版面配置程序

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

  1. 上層轉譯器會確定自己的寬度。
  2. 家長會超過孩童,並:
    1. 放置子項轉譯器 (設定其 x 和 y)。
    2. 必要時呼叫子項版面配置,例如:髒汙或位於全域版面配置中,或基於其他原因計算子項的高度。
  3. 父項使用子項的累積高度、邊界和邊框間距自行設定高度 - 父項轉譯器的父項會使用這個項目。
  4. 將骯髒位元設為 false。

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

Firefox 版面配置的輸出內容是一項「指標」object(nsHTMLReflowMetrics)。其中包含轉譯器計算出的高度。

寬度計算

系統會使用容器區塊的寬度 (轉譯器的樣式「寬度」) 計算轉譯器的寬度邊界和邊界。

例如,下列 div 的寬度:

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

這個值是由 WebKit 計算,如下所示(class RenderBox 方法 calcWidth):

  • 容器寬度為容器 availableWidth 和 0 的最大值。 在本例中,可用寬度為 contentWidth,計算方式如下:
clientWidth() - paddingLeft() - paddingRight()

clientWidth 和 clientHeight 表示物件的內部 但邊框和捲軸除外。

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

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

目前為止計算的是「偏好寬度」。 現在會計算最小和最大寬度。

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

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

換行字元

當版面配置中間的轉譯器決定需要中斷的情況下,轉譯器就會停止,並套用到需要損毀的版面配置父項。 父項會建立額外的轉譯器,並對其呼叫版面配置。

繪畫

在繪畫階段中,系統會掃遍算繪樹狀結構,並轉譯器的「 Paint()」方法,以在螢幕上顯示內容。 繪製功能會使用 UI 基礎架構元件。

全球規模和增量

繪畫就像佈局外界,為整棵樹 (或漸進式) 繪製也可以是通用的。 在漸進式繪畫中,部分轉譯器的改變方式不會影響整個樹狀結構。 變更的轉譯器會讓畫面上的矩形失效。 這會導致 OS 發現該區域為「骯髒的區域」產生「顏料」活動。 作業系統會巧妙地將多個區域合併為單一區域, 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... 等個方塊。

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

範例:

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」設定屬性和「浮點值」屬性。

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

靜態位置未定義位置,且會使用預設定位。 在其他配置中,作者會指定位置:上、下、左、右。

盒子的配置方式取決於:

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

方塊類型

區塊方塊:形成區塊 - 在瀏覽器視窗中有自己的矩形。

方塊。
圖 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 之前,並且是在一般流程中經過繪製,但是 Z-index 屬性較高,所以在根方塊所持有的堆疊中,它的距離會更高。

資源

  1. 瀏覽器架構

    1. Grosskurth,阿俊。網路瀏覽器的參考架構 (pdf)
    2. Gupta、Vineet。瀏覽器的運作方式 - 第 1 部分 - 架構
  2. 剖析

    1. Aho、Sethi、Ullman、Compilers:原則、技術與工具 (亦稱為「Dragon Book」),Addison-Wesley,1986 年
    2. Rick Jelliffe。大膽搶眼:HTML 5 推出兩份新草稿。
  3. Firefox

    1. L. David Baron,加快 HTML 和 CSS:網頁開發人員的版面配置引擎內部
    2. L. David Baron,Faster HTML and CSS: Layout Engine Internals for Web Developers (Google 技術介紹影片)
    3. L. Mozilla 的版面配置引擎 David Baron
    4. L. David Baron,Mozilla 樣式系統說明文件
    5. Chris Waterson,HTML Reflow 注意事項
    6. Chris Waterson,Gecko Overview
    7. Alexander Larsson,HTML HTTP 要求的生命週期
  4. WebKit

    1. David Hyatt,導入 CSS(第 1 部分)
    2. David Hyatt,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

翻譯

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

您可以在這個頁面上查看 韓文土耳其文

謝謝大家!