【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

toddmark發表於2019-03-30

原文地址

( 譯者注:這是一篇深度好文,並且附帶官方簡體中文。本次的翻譯為一人完成,限於水平,難免有誤,故需要學習本文內容的同學請直接參考原文地址進行閱讀。

導讀: 終於,我在一週之內講這長篇大論的瀏覽器背後的故事翻譯完了。如果再要我重新閱讀一遍,可能需要我沐浴焚香般的準備。現在我忍著肩膀和手腕的痠痛,寫下發布前的最後一些體會:

  1. 這是一篇長度不小的文章。但是整個文章的內容可以說已經十分精煉,是一個以色列開發者在查閱數百萬行 c++ 程式碼和無數文件的凝結之作。希望想更深入瞭解瀏覽器內部的讀者們收藏原文。
  2. 儘管翻譯結束了,但是我仍然需要好好消化一下文章內容。本來想挑出一部分關鍵內容供大家參考。但實在難以取捨。所以想要了解文章內容的同學,請快速閱覽目錄進行檢索。
  3. 這篇文章,應該是我近一個月以來最長的一篇了。手痠,就寫到這裡吧。 )

序言

這是篇全面介紹 WebKit 和 Gecko 的內部操作的文章,它是以色列的開發者 Tail Garsiel 的大量的研究成果。過去幾年,她重新審視了已公開的關於瀏覽器內部的資料(參考資料)同時花費了很多時間去閱讀 web 瀏覽器原始碼。她寫道:

在 IE 90% 支配的那個年代,把瀏覽器當做一個“黑盒”再也合適不過了,但是現在,開源瀏覽器佔據了一半的市場份額,是時候去了解引擎的背後同時看看 web 瀏覽器的內部。儘管,裡面有數百萬行 C++ 程式碼……

Tail 在她的網站上釋出了她的研究,但是我們想讓更多的人知道,所以我們已經重新整理再次釋出在這裡。

作為一個 web 開發,瞭解瀏覽器操作的內部會幫助你做出更好的決定,同時在最佳開發實踐時瞭解充分的理由。而這是一篇有長度的文章,我們推薦你花點時間去深挖探究,我們保證你會對自己的所做滿意。 Paul lrish, Chrome 開發人員關係部

這篇文章被翻譯成幾種語言:HTML5 Rocks 翻譯了 德語,西班牙語,日語,葡萄牙語,俄語和簡體中文版本。你也可以看到韓語和土耳其語。 你也能看看關於這篇主題 Tail Garsiel 在 Vimeo 上的談話

(譯者注:這篇目錄翻譯了我半個小時,通過目錄的回顧確實跟之前的一些零碎知識串聯了起來,發現很多。更主要的是,跑個題來緩解下被這個目錄嚇尿的心臟。)

目錄

Web 瀏覽器是使用最廣泛的軟體。這篇讀物中,我會解釋在場景之後他們是如何工作的。我們將會看到,當你在位址列輸入 google.com 時直到在瀏覽器螢幕上看到 Google Page 頁面後,發生了什麼。

1.介紹

1.我們將要討論的瀏覽器

如今常用的主要瀏覽器有 5 種: Chrome,IE,火狐,Safari 和 Opera。在移動端上,主要的瀏覽器是安卓瀏覽器,蘋果,Opera 迷你和 Opera移動端還有 UC 瀏覽器,諾基亞 S40/S60 瀏覽器和 Chrome 也都是,除了 Opera 瀏覽器,其他都是基於 WebKit。(譯者注:前一句話在官方簡體中文裡沒有.)我從開源瀏覽器火狐和 Chrome 以及 Safari(部分開源)中距離。根據 StatCounter 統計(從2013年6月以來) Chrome 火狐和 Safari 組成了全球桌面瀏覽器使用量的 71%。在移動端,安卓瀏覽器,iPhone 和 Chrome 有 54% 的使用率。

2.瀏覽器的主要功能

瀏覽器的主要功能是展示你選擇的 web 資源,通過服務端的請求然後在瀏覽器視窗展示。這個資源通常是一個 HTML 文件,但也有可能是一個 PDF,一張圖片,或者其他型別的內容。資源的位置通過使用者使用 URI(Uniform Resource Identifier) 來明確指出。

瀏覽器插入和展示 HTML 檔案的方式在 HTML 和 CSS 規範中有詳細說明。這些規範通過 W3C(World Wide Web Consortium) 維護,這些規範也是 web 的標準組織。這些年的瀏覽器只是遵守一部分標準同時開發了他們自己的擴充套件。這對 web 開發者來說引發了一系列的相容性問題。如今大多數瀏覽器或多或少遵守這些規範。

瀏覽器使用者介面相互有很多共同之處。它們之間的共同元素有:

  • 用於插入 URL 的位址列
  • 前進和後退按鈕
  • 書籤選擇
  • 用於重新整理和停止載入當前文件的重新整理和停止按鈕
  • 帶你去主頁的主頁按鈕

奇怪的是,瀏覽器的使用者介面沒有任何形式的規範,它只是從過去幾年的經驗和通過瀏覽器相互模仿中產生的最佳實踐。 HTML5 規範沒有定義一個瀏覽器必須擁有的 UI 元素,但是列出了一些常見元素。它們有位址列,狀態列和工具欄。這些內容,尤其是,像火狐的下載管理器對具體瀏覽器而言是特有的。

3.瀏覽器的上層結構

瀏覽器的主要元件有(1.1):

  1. 使用者介面:這包括位址列,前進/後退按鈕,書籤選單等等。每個部分除了你請求頁面的的視窗都會顯示。
  2. 瀏覽器引擎:在 UI 和渲染引擎之間的統一行為
  3. 渲染引擎:對展示請求的內容響應。比如請求的內容是 HTML,這個渲染引擎會解析 HTML 和 CSS,同時在螢幕上展示解析後的內容。
  4. 網路:對於網路呼叫比如 HTTP 請求,在獨立的平臺介面下對不同的平臺使用不同的實現。
  5. UI 後臺:用於繪製像組合盒子和視窗的基本元件。這個後臺暴露的通用介面不是平臺特有的。在這之下它使用了作業系統使用者介面的方法。
  6. JavaScript 直譯器:用於解釋和執行 JavaScript 程式碼
  7. 資料儲存:這是持續存在的一層。瀏覽器可能需要本地化儲存資料的順序,比如 cookies。劉安琪也支援 storage 機制比如 LocalStorage,IndexDB,WebSQL 和 檔案系統。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:瀏覽器元件

對於瀏覽器而言這是很重要的,比如 Chrome 執行多個渲染引擎的例項為每一個標籤。每個標籤有個獨立的程式。

2.渲染引擎

渲染引擎的責任是,額……渲染,也就是在瀏覽器螢幕上展示請求的內容。

預設的渲染引擎可以展示 HTML 和 XML 文件以及圖片。它也可以通過外掛或者擴充套件來展示其他的資料型別。舉個例子,使用 PDF 檢視外掛展示 PDF 文件。然而,在本章節中我們將關注它的主要用處:展示使用 CSS 格式化的 HTML 和 圖片。

1.渲染引擎

不同的瀏覽器使用不同的渲染引擎:IE 使用 Trident,火狐使用 Gecko,Safari 使用 WebKit。Chrome 和 Opera(自 15 版)使用 Blink,是Webkit 的一個分支。

WebKit是一個開源引擎,作為引擎,最開始在 Linux 平臺上然後被 Apple 為了支援 Mac 和 Windows而修改。瞭解webkit.org的更多細節。

2.主要過程

渲染引擎從網路層開始獲取請求文件的內容。這個通常在一個 8KB 塊中完成。

在那之後,展示了渲染引擎的基本流程:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:渲染引擎基本工作流

渲染引擎開始解析 HTMl 文件同時在一個名叫“內容樹”的樹中轉化元素變成 DOM 節點。引擎將會解析樣式資料,外部的 CSS 檔案和元素樣式。在 HTML 中帶有可視指令的樣式資訊將會被用於建立另一個樹:渲染樹

渲染樹包含了有可視屬性像是顏色和尺寸的矩形。這個矩形在螢幕上以正確的順序展示。

之後的渲染樹會通過一個“佈局”程式。這意味著該程式會給在螢幕上應該出現的每個節點一個精確的座標。下一個階段是繪製——渲染樹被轉換然後每個節點通過 UI 後臺層被繪製。

理解這個漸進過程是非常必要的。為了更好地使用者體驗,渲染引擎會盡快嘗試在螢幕上展示內容。它在開始構建和佈局渲染樹之前,不會等待所有的 HTMl 被解析。一部分內容被解析和展示,而程式繼續剩餘的從網路來的內容。

3.主要過程例子

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:WebKit 主要流程

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

3.6

火狐的 Gecko 渲染引擎主要流程

從圖 3 和圖 4你可以看到,WebKit 和 Gecko 術語上有點不同,過程還是基本一樣的。

Gecko 呼叫一個樹,這個樹是視覺化的一個被格式化的“框架樹”。每個元素是一個框架。WebKit 使用的叫做“Render Tree”,同時它由“Render Objects”組成。WebKit 對元素位置使用“Layout”,而 Gecko 稱它為 “Reflow”。“Attachment”是WebKit一個術語,用於連線 DOM 節點和視覺化資訊用於建立渲染樹。一個不重要的非語義的不同是 Gecko 在 HTML 和 DOM 樹之間有一個額外的層,叫做 “content sink(內容沉澱)”,同時它是一個製作 DOM 元素的工場。我們將會討論過程的每個部分:

3.解析和 DOM 樹結構

1.一般解析

因為在渲染引擎中,解析是非常明顯的程式,我們將會探索的深入一點。通過關於解析的簡單介紹來開始。

解析一個文件意味著翻譯為程式碼可用的結構。解析的結果通常是一個節點樹,這顆樹代表著文件結構。通常叫做解析樹或者語法樹。

舉個例子,解析表示式 2 + 3 -1 可以返回下面的樹:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:數學表示式的節點樹

1.語法

解析基於檔案遵守的語法規則:語言或者寫入的格式。所有可以解析的格式必須由詞彙和句法規則構成確定的語法。這稱為上下文無關語法。人類語言不是這種語言,因此不能用常規的解析技術解析。

2.解析器——詞法分析器混合

解析可以分為兩個獨立的子過程:詞法分析和語法分析。

詞法分析是將輸入變成為標記的過程。標記是語言詞彙:構建塊的集合。在人類語言中它由所有在這個語言的字典中出現的單詞構成。

語法分析是語言語法規則的應用。

解析通常在兩個部分中獨立的工作:詞法分析器(有時也叫標記分析器),負責將輸入變成有效地標記,同時解析器的責任是通過根據語言規則來分析文件構建解析樹。詞法分析器知道如何去除不相關的字元比如空格和換行。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:從源文件到解析樹

解析過程是反覆的。解析器通常為新的標記向詞法分析器請求,並嘗試將標記與某條語法規則匹配。如果規則匹配了,一個相應標記的節點將被新增到語法樹中去,並且解析會請求下一個標記。

如果沒有規則匹配,解析器會內部儲存這個標記,並且保持請求標記直到一個匹配到所有儲存在內部的標記的規則被發現。如果沒有找到規則,那麼解析器將丟擲一個錯誤。這意味著檔案無效並且包含語法錯誤。

3.翻譯

在很多例子中,解析樹不是最終產物。解析通常用於翻譯:轉換輸入文件為另一種格式。舉個例子比如編譯:編譯器編譯原始碼成為機器碼,首先編譯成編譯樹,然後再編譯成機器碼檔案。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:編譯過程

4.解析舉例

在圖表5中,我們從數學表示式中構建編譯樹。我們試試定義一個簡單的數學語言然後看看編譯過程。

詞彙:我們的語言包括數字,加減號。 語法:

  1. 語言語法組成了表示式,項和操作符。
  2. 語言包括任何數字表示式。
  3. 作為項的表示式通過連結另一個項的“操作符”連結。
  4. 操作符是加號或者減號標記。
  5. 項是數字或者表示式。

我們來分析輸入的: 2 + 3 - 1

首先匹配到的規則串是 2:根據規則 #5 這是一個項。第二個匹配是 2 + 3:這個匹配了第三個規則:一個項鍊接著一個連結另一個項的操作符。下一個匹配將在輸入的最後。2 + 3 -1 是一個表示式,因為我們知道, 2 + 3 是一個項,所以我們有一個項,通過連結另一個項的操作符連結著。2 + + 不會匹配任何規則,因此是一個無效的輸入。

5.變數和語法的定義格式

詞彙通常通過正規表示式表現。

舉個例子,我們的語言將被定義為如下:

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
複製程式碼

正如你看到的,整型通過正規表示式定義。

語法通常通過一種叫做 BNF 的格式定義。我們的語言將被定義為如下:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression
複製程式碼

我們認為,如果一種語言的語法是上下文無關的,那麼它可以通過常規解析器解析。上下文無關語法的直觀定義是可以在 BNF 中被完全表達的語法。一種形式的定義是關於上下文無關文法的維基百科的文章。

6.直譯器型別

這裡有兩種解析型別:自頂向下和自底向上的解析。直觀的解釋是,自頂向下的解析檢查上層語法結構然後嘗試找到規則匹配。自底向上從輸入開始,逐級轉化成語法規則,從底層規則開始直到遇到頂層規則。

我們看看這兩種解析型別如何解析我們的例子。

自頂向下解析從上層開始:它將識別 2 + 3為一個表示式。然後識別 2 + 3 - 1 為一個表示式(識別表示式的過程是逐步的,匹配其他規則的,但是是從上層開始。)

自底向上解析將會瀏覽輸入直到一個規則被匹配。它將用這個規則替換匹配。這會一直執行直到輸入的末端。部分匹配到的表示式在解析棧上被替換。

Stack Input
term 2 + 3 -1
term operation + 3 - 1
expression 3 - 1
expression operation - 1
expression -

這種自底向上的解析稱為移入規約解析,因為輸入偏移到右側(想象一個指標在輸入開始然後移動到右側),並且逐漸減少語法規則。

7.自動生成解析

有個工具可以生成解析。你告訴工具你的語言語法——它的詞彙和語法規則——然後工具會生成一個有效解析。建立解析需要深刻理解解析,並且手動建立一個優化的解析並不容易,所以解析生成器是很有用的。

WebKit 使用兩個著名的解析工具: Flex,用於建立詞法,Bison,用於建立解析(你會或許把他們稱為 Lex 和 Yacc)。Flex 輸入是一個檔案,包含標記的正規表示式定義。Bison 的輸入是在 BNF 格式中的語言與法。

2.HTML 解析

HTML 解析器的工作是把 HTML 標記轉化成解析樹。

1.HTML 語法定義

HTML 的詞彙和語法由 W3C 組織創造,在這個規範被定義。

2.不是上下文無關文法

如同我們在解析中的介紹,語法可以像 BNF 那樣使用格式化的格式被定義。

不幸的是,所有常規的解析方式不能應用於 HTML(為了好玩我不把它們現在引入——它們在解析 CSS 和 JavaScript時將被用到)。HTML 不能像解析器需要的那樣通過上下文無關文法被定義。

乍一看這個表現很奇怪; HTML 十分像 XML。有非常多的 XML 解析器可以使用。HTML 是 XML 的變體——所以有什麼很大的不同嗎?

這裡的不同在於,HTML 儘可能的更多“包容”:它能讓你省略某些標籤(那些被隱式新增的),或者有時候省略開始或結束標籤等等。整體來看它是“軟性語法,與 XML 的嚴格硬性語法不同。

3.HTML DTD

HTML 定義在一種 DTD 格式裡。這種格式用於定義 SGML 家族的語言。這個格式為所有允許的元素,它們的屬性和等級定義。我們之前看到,HTML DTD 不是一種上下文無關文法。

DTD 有一些變體。嚴格模式適用於唯一的規範,但是其他模式包含對過去瀏覽器使用的標記的支援。這個目的是可以向後相容老舊的內容。目前嚴格模式的 DTD 參考這裡www.w3.org/TR/html4/st…

4.DOM

輸出樹(解析樹)是 DOM 元素和節點屬性的樹。DOM 是 Document Object Model 的縮寫。它是 HTML 文件的表現物件和 HTML 元素對外部世界像是 JavaScript 元素的介面。樹的根節點是 “Document” 物件。

DOM 對標記來說幾乎要一對一的關係。舉個例子:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>
複製程式碼

標記將會被轉化為下面的 DOM 樹:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:例子中的 DOM 樹

像 HTML,DOM 通過 W3C 組織定義。參考這裡www.w3.org/DOM/DOMTR。它是對操作文件的一般定義。特殊的模型描述了 HTML 特殊元素。HTML 定義可以在這裡找到:www.w3.org/TR/2003/REC…

當我談到樹包含 DOM 節點時,我的意思是這棵樹是有結構的元素,實現了 DOM 其中之一的介面。瀏覽器混合了這些實現,這些實現有一些通過瀏覽器內部定義的其他屬性。

5.解析演算法

如我們之前看到的部分一樣,HTML 不能使用常規的自頂向下或者自底向上解析。

原因有:

  1. 語言的包容性
  2. 事實是,瀏覽器有錯誤容忍的傳統,為了支援常見的無效的 HTML 的情況。
  3. 解析過程是不斷重複的。對於其他語言,原始碼在解析的時候不會改變,但是 HTML,動態程式碼(比如包含 document.write() 的指令碼元素呼叫)可以新增額外的標記,所以解析過程實際上修改了輸入。

不能使用常規解析技術,瀏覽器為解析 HTML 建立了自定義解析。

HTML5 規範定義瞭解析演算法的細節。演算法有兩個階段組成:標記(斷詞)和結構樹。

標記是詞法分析,解析是輸入變成標記。在 HTML 中,標記是開始標籤,結束標籤,屬性名和屬性值。

標記器識別標記,把標記給樹構造器,並且為下個識別的標記處理下個字元,直到輸入的結尾。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:HTML 解析過程(來自 HTML5 定義)

6.斷詞(標記)演算法

這個演算法的輸出是 HTML 標記。這個演算法被作為狀態機表達。每個狀態使用一個或者多個輸入流的字元,並且根據這些字元更新下一個狀態。這個決定通過當前標記狀態和樹構造狀態影響。這就意味著消耗同樣的字元為了正確的下個狀態將會產出不同的結果,這取決於當前狀態。這個演算法過於複雜,以致不能完全描述,我們來看看一個簡單的例子,這可以幫助我們理解這個規則。

基本例子:標記以下 HTML:

<html>
  <body>
    Hello world
  </body>
</html>
複製程式碼

初始化狀態是 “Data State”。當遇到 < 字元時,狀態變成“Tag open state”。使用 a-z 的字元產生“Start tag token”的建立,狀態變為“Tag name state”。我們保留這個狀態直到 > 字元出現。每個字元都被新增到新的標記名稱上。在我們的例子中,這個建立的標記是 html 標記。

> 標籤出現,當前標記被髮送,同時狀態變回 Data state<body> 標籤也是用相同的步驟處理。目前為止,htmlbody 標籤被髮送了。我們現在回到了 “Data state”。遇到 Hello world 字元的 H 將會引起建立和字元標記的傳送,這將一直進行直到遇見 </body><。我們將為 Hello world 的每一個字元傳送一個字元標記。

現在我們回到“Tag open state”。遇到下一個輸入 / 將會引起結束標籤的建立,並且移動到“Tag name state”。再一次我們保持在這個狀態,直到我們遇見 >。此時這個新的標籤標記將被髮送,並且我們回到“Data state”。</html> 輸入將像之前的例子一樣被處理。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

標記案例輸入

7.樹構造器演算法

當建立文件物件的解析器被建立。在樹構造階段期間,以 Document 為根節點的 DOM 樹也被修改,並且元素被新增進去。通過標記生成器傳送的每個節點將被樹構造器處理。對於每個標記,規範定義了 DOM 元素與之相關,同時有一個開放元素的棧。這個棧用於正確巢狀沒有匹配和沒有閉合的標籤。這個演算法也是作為一個狀態機描述。這個狀態叫做“insertion modes”(插入模式)。

我們看看樹構造過程輸入的例子:

<html>
  <body>
    Hello world
  </body>
</html>
複製程式碼

從標記階段中,輸入給樹構造器的階段是連續的標記。初始模式是 “initial mode”。接收到 “html” 標記將會移動到 “before html” 模式,並且在那個模式中再處理標記。這將引發 HTMLHtmlElement 元素建立,它將被新增到 Document 物件的根節點中。

狀態將被變為 “before head”。“body” 標記被接受時。HTMLHeadElement 將被隱式建立,儘管我們沒有 “head” 標記,並且它會被新增到樹中。

現在我們移動到 “in head” 模式,並且將會到 “after head”。body 標記是再次執行的,HTMLBodyElement 被建立和插入,模式被轉換為 “in body”。

“Hello world” 字串的字元標記現在接受了。首先會發生建立和 “Text” 模式的插入,並且其他字元也將加入到這個節點中。

body 結束標記引起到 “after body” 模式的轉換。我們會收到 html 結束標記,它將會移動到 “after after body” 模式。接受檔案標記的結束將會結束解析過程。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

html 例子的樹構造

8.解析結束的行為

在這個階段,瀏覽器將會作為互動而標記文件,並且開始解析在 “deferred” 模式下的指令碼:這些本應該在文件解析後被解析。文件狀態將被設定為 “complete”然後一個 “load” 事件將被觸發。

在這裡能看到 HTML5 規範中標記器和樹構造器的完整演算法

9.瀏覽器容錯度

你不會在 HTML 頁面得到一個 “無效語法” 的錯誤。瀏覽器會修復任何無效的內容,然後繼續。

比如下面的例子:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>
複製程式碼

我一定要違反很多規則(“mytag” 不是個標準標準標籤,“p” 和 “div” 標籤的錯誤巢狀等等)但是瀏覽器仍然正確展示,並且沒有任何抱怨。以為很多解析程式碼在修復 HTML 作者的錯誤。

錯誤處理是十分一致的,但吃驚的是,它不是 HTML 規範的部分。如同書籤和後退前進按鈕一樣,它只是這些年在瀏覽器中被髮開出來。很多網站上有很多無效的 HTML 結構重複著,並且瀏覽器嘗試用一種與其他瀏覽器一樣的方式修復。

HTML 規範定義了一些要求。(WebKit 在 HTML 解析器類的開始的註釋很好的總結了)

解析器解析標記輸入成為文件,構建文件樹。如果文件格式良好,解析會直接進行。

不幸的是,我們不得不處理很多 HTML 文件,那些文件沒有很好的格式,所以解析器不得不容忍這些錯誤。

我們可以瞭解到至少以下幾種錯誤條件:

1. 在某些標籤外部,明確禁止新增元素。這種情況下,我們應該關閉所有的標籤,直到一個禁止的標籤出現,之後新增它。

2. 我們不允許直接新增元素。它可能是人們寫入文件忘記的標籤(或者其中的標籤是可選的)。比如以下標籤:HTML HEAD BODY TBODY TR TD LI(漏了什麼嗎?)

3. 我們想在行內元素中新增塊元素。閉合所有的行內元素直到下一個更高階的塊元素出現。

4. 如果這些都沒有作用,直到允許我們新增或者忽略標籤才會閉合元素。
複製程式碼

我們來看看 WebKit 的容錯例子:


替換

有些網站使用 </br> 而不是 <br>。為了相容 IE 和 火狐, 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);
}
複製程式碼
深度標籤層級

註釋不言而喻。

www.liceo.edu.mx 是一個網站的例子,這個網站簽到了大約 1500 個標籤層級,所有這些來自 <b> 分支。在全部忽略它們之前,我們最多允許 20 個同型別的巢狀標籤。

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 結束標籤

再次參考註釋:

為了支援被破壞的 HTML。我們永遠不會閉合 body 標籤,因為有些愚蠢的網站頁面在文件真正結束之前閉合了它。我們依賴在 end() 呼叫上閉合它們。

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;
複製程式碼

所以 web 作者意識到——除非你想去表現一個 WebKit 容錯程式碼片段作為例子——否則就寫良好格式化的程式碼。

3.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" 是 identifier的 縮寫,類似類名。“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 代表空格)被一系列選擇符分割。一個規則集包含花括號和內部的描述或者可選的逗號分割。

1.WebKit CSS 解析

WebKit 使用 Flex 和 Bison。如同從解析介紹中回憶起來的一樣,Bison 建立了自底向上遞迴下降解析。火狐使用了自頂向下手工寫入。這兩種例子的每個 CSS 檔案會被解析成一個 StyleSheet 物件。每個物件包含 CSS 規則。這個 CSS 規則物件包含選擇器和物件宣告以及其他對應 CSS 語法的物件。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:解析 CSS

4.指令碼和樣式表的執行順序

1.指令碼

web 的模型是同步的。建立者希望當解析器遇到 <script> 標籤被解析後立即執行。文件解析器中止直到指令碼被執行。如果是外部指令碼,那麼資源首先必須從網路請求——這也是同步處理的,同時直到資源獲得,否則解析一直中止。這個模型有許多年了,同時在 HTML4 和 HTML5 中被定義。創作者可以給指令碼新增 “defer” 屬性,在這種情況下,這將不會終止文件解析,並且在文件解析後執行指令碼。HTML5新增一個可選標記給指令碼作為非同步標記,以便將來解析和通過不通執行緒執行。

2.推斷解析

Webkit 和 Firefox 都做了這種優化。當指令碼執行時,另一個執行緒解析剩餘的文件,並且找出從網路上需要載入的其他資源然後載入它們。用這種方式,資源可以在平行連線上載入,並且總的來說速度是提升的。注意:推斷解析只解析外部資源像是外部指令碼,樣式表和圖片:它不會修改 DOM 樹——這留給主要解析器。

3.樣式表

另一方面樣式表有著不同的模型。概念上來說因為它看起來並不修改 DOM 樹,所以沒有理由去等待他們和停止文件解析。這裡有個問題,即在文件解析階段,為樣式資訊的指令碼請求。如果樣式沒有載入和解析,指令碼將會得到錯誤答案,並且顯然這會引起一系列問題。這看起來是個邊緣問題,但事實上很常見。當有樣式表仍然在載入和解析的時候,火狐阻止了所有的指令碼。WebKit 只會阻止當嘗試去訪問某些樣式屬性,而這些屬性可能被未載入的樣式影響的指令碼。

4.渲染樹構造器

當 DOM 樹被構建時,瀏覽器構建另一個樹,是渲染樹。這是棵可視元素按照展示順序排列的樹,是視覺化文件的表現。這棵樹的目的是可以在它們正確的順序下繪製內容。

火狐在渲染樹的 “frames” 中呼叫元素。 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 節點,根據 display 的屬性:

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中,如果元素想去建立特殊渲染,它會覆寫 createRender() 方法。渲染指向樣式物件,這個物件不包含幾何資訊。

1.渲染樹和 DOM 樹的關係

渲染對應著 DOM 元素,但並不是一對一的乾洗。不可見的 DOM 元素不會插入到渲染樹中。舉個例子 “head” 元素。那些 display 值是 “none” 的元素也不會出現在樹中(但是 visibility 是 “hidden” 的元素會出現)。

DOM 元素對應一些可視物件。常見的元素帶有複雜的結構,它們不能通過一個矩形描述。比如: “Select” 元素有三個渲染:一個用於展示區域,一個用於下拉選單,還有一個用於按鈕。當文字換行時也一樣,因為寬度不滿足一行,新的行必須作為額外的渲染新增。

多行渲染的另一個例子是破壞的 HTML。根據 CSS 定義,內聯元素要麼只包含塊元素要麼只包含內聯元素。在混合內容的例子中,匿名塊將被建立用於包含內聯元素。

一些渲染物件對應 DOM 節點,但是不在樹中同樣的位置上。浮動和絕對定位元素不在流上,被放在了樹的不同部分,對映在真實的結構上。一個佔位結構在它們應該在的位置上。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:渲染樹和對應的節點樹。“viewport” 是初始化包含塊。在 WebKit 中它是 “RenderView” 物件。

2.樹構建的過程

在火狐中,表現層被當做監聽註冊在 DOM 更新上。表現層給 FrameConstructor 委託框架建立,並且構造器解析樣式(參考樣式計算)和建立框架。

在 WebKit 處理樣式和建立渲染層的過程叫做 “attachment”。每個 DOM 節點都有一個 “attach” 方法。Attachment 是同步的,節點插入給 DOM 樹呼叫新節點的 “attach” 方法。

處理 html 和 body 標籤的結果是渲染樹根節點的構造。根渲染物件對應 CSS 中叫做包含塊的規範:最頂部的包含其他所有塊的塊。它的尺寸是視窗:即瀏覽器視窗的區域尺寸。火狐稱作 ViewPortFram 而 WebKit 稱作 RenderView。這是文件指向的渲染物件。其餘的樹作為 DOM 節點插入被構造。

參考 CSS2 規範的過程模型

3.樣式計算

構建渲染樹需要計算每一個渲染物件的可視屬性。這通過計算每個元素的樣式屬性來完成。

樣式包括各種來源的樣式表,內聯樣式元素和 HTML 中的視覺化屬性(像是 “bgcolor” 屬性)。之後被翻譯成匹配 CSS 樣式屬性。

樣式表的來源有,瀏覽器預設樣式,網頁創作者提供的樣式,和使用者樣式——這些樣式表通過瀏覽器使用者提供(瀏覽器允許你定義自己喜歡的樣式。在火狐中,初始化,通過放在 “Firefox Profile”資料夾中的樣式完成)。

樣式計算有點困難:

  1. 樣式資料是很大的結構,控制非常多的樣式屬性,這可能引起記憶體問題。
  2. 為每個元素查詢匹配規則可能引起效能問題,在沒有優化的情況下。遍歷每個元素的全部規則去找到匹配的內容是一件繁中的任務。選擇器有複雜的結構,這就導致佩佩過程看起來是有效地路徑,其實是無效的,然後不得不去嘗試另一條路徑。 舉個例子——這個混合的選擇器:
    div div div div {}
    複製程式碼
    意味著這個規則會應用於 3 個 <div> 的後代。假設你想去檢查是否這個規則應用於一個 <div> 元素。你可以選擇某條樹上向上路徑去檢查。你也可以傳過節點樹向上發現只有兩個 div,然後規則並不適用。你這是需要去嘗試另一顆樹。
  3. 應用規則包含了十分複雜的層疊規則,這個規則定義了規則的等級。

我們看看瀏覽器如何面對這些問題:

1.共享樣式資料

WebKit 節點引用樣式物件(RenderStyle)。這些物件可以通過節點在相同的條件下共享。這些節點是兄弟或者表兄弟並且:

  1. 元素必須在相同的滑鼠狀態(比如:不能一個狀態是 :hover 而另一個不是)
  2. 任何元素不能沒有 id
  3. 標籤名應該匹配
  4. 類屬性應該匹配
  5. 對映屬性的集合完全相同
  6. 連結狀態必須匹配
  7. 焦點狀態必須匹配
  8. 任何元素應該不被屬性選擇器影響,這裡的影響定義為在任何位置使用了選擇器的,使用屬性選擇器的任何選擇器匹配。
  9. 元素上不應該有內聯樣式屬性。
  10. 不應該有兄弟節點匹配。WebCore 簡單地引發一個全域性開關,當任何兄弟選擇器相遇時,並且當他們存在時時對全部文件禁用樣式共享。這包括 + 選擇器和像 :first-child 和 :last-child 選擇器。

2.火狐規則樹

火狐有兩顆額外的樹用於簡化樣式計算:一顆規則樹和樣式上下文樹。WebKit 也有樣式物件,但是沒有像樣式上下文樹來儲存,只有 DOM 節點指向相關樣式。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:火狐樣式上下文樹

樣式上下文包含結束值。這個值被應用在所有正確順序下的匹配規則和實行從邏輯到實際值的轉換操作而計算。舉個例子,如果邏輯值是螢幕上的百分比,它將被計算和轉換成絕對單位。這個規則樹的注意很聰明。它可以在節點中分享這些值,避免重複計算。這也節約了空間。

所有的匹配規則儲存在一棵樹中。路徑上的底部節點有更高的優先順序。樹包含所有的路徑,為了匹配已經發現的規則。儲存這些規則是懶處理的。樹不會在每個節點開始的時候計算,但無論何時一個節點樣式需要計算時,計算路徑被新增到樹中。

這個點子看樹像是在詞法中看單詞。我們看看已經計算的規則樹:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

假設我們需要為上下文樹的其他元素匹配規則,並且找出匹配規則(正確的順序)是 B-E-I。我們已經有在樹中的路徑,因為我們已經計算出了路徑 A-B-E-I-L。我們將減少我們的工作。

來看看樹如何節約我們的工作。

1.結構分隔

樣式內容被分割成結構。這些結構包含了樣式資訊,像是邊框和顏色這種種類。結構中的所有屬性是繼承或者不繼承的。除非元素定義了繼承屬性,否則從父級繼承。如果沒有定義繼承屬性,使用預設值。

2.使用規則樹計算樣式上下文

當為某個元素計算樣式上下文時,我們首先計算規則樹中的路徑或者使用已經存在的。接著我們開始在路徑中應用規則去在我們新的樣式上下文中應用規則。我們開始從路徑底部節點——這個節點由最高的優先順序(通常是最特殊的選擇器)和穿過樹到頂部直到我們的結構被填滿。如果在規則節點的結構上沒有定義,我們可以很好地優化——我們到樹上直到我們發現一個節點,是充分定義和簡單指向它——這是最好的優化——真個結構被共享。這節約了末尾值的計算和記憶體。

如果我們發現部分定義,我們到樹上填滿。

如果在結構上找不到任何定義,有些例子中結構是 “繼承” 型別,我們在上下文樹中指向我們父級的結構。在有些例子中我們也成功共享了結構。如果預設值被使用它就是重置結構。

如果大部分節點沒有新增值,那麼我們需要做一些額外的計算,為它轉換成實際值。我們接著在樹節點中快取結果為了讓後代使用。

此例中元素有一個兄弟節點,它指向同一個樹節點,那麼全部樣式上下文可以在它們之間共享。

假定我們有如下 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}
複製程式碼

為了簡化這些事我們需要只填滿這兩種結構:顏色結構和邊距結構。顏色結構包括只包括一個成員:顏色。邊距結構包含四個方面。

這個結果規則樹看起來是這樣(節點被節點名稱標記:它們指向規則的數量):

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:規則樹

上下文樹將看起來像這樣(節點名:它們指向規則節點):

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:上下文樹

假設我們解析 HTML 並且得到第二個 <div> 標籤。我們需要為這個節點建立樣式上下文和填充它的樣式結構。

我們匹配這些規則並且找出 <div> 匹配規則是 1,2 和 6的。這意味著在樹中已經有存在的路徑,我們的元素可以使用這些路徑,並且我們為規則 6 只需要新增另一個節點給它(在規則樹中的節點 F)。

我們將建立規則上下文並且在上下文樹中放置。新的上下文內容將會在規則樹中指向節點 F。

我們需要填充樣式結構。我們從填滿邊距結構開始。因為最後一個規則節點(F)沒有新增到邊距結構中,我們可以在樹上直到找到在之前節點插入的快取結構然後使用它。我們將發現它在節點 B 上,它是定義的邊距規則的最高節點。

在第二個 <span> 元素上的工作相對容易。我們匹配到規則然後得出指向規則 G 的結論,像是之前的 span。因為我們有一個兄弟節點指向相同的節點,我們可以共享全部樣式上下文,然後指向之前 span 的上下文。

對於整合自父級的包含規則的結構,快取在上下文樹中被處理(顏色屬性實際上是繼承,但是火狐當做預設對待然後快取在規則樹上)。

如果我們在段落裡為字型新增規則作為例項:

p {font-family: Verdana; font size: 10px; font-weight: bold}
複製程式碼

接著這個段落元素,它是在上下文樹的div的子代,作為它的父級它可以共享相同字型。這是在段落中沒有字型規則定義的情況。

在 WebKit 中,那些沒有規則樹的,匹配宣告會轉化四次。首先非重要高階優先權屬性被應用(屬性應該是第一次應用因為其他依賴它們,比如 display),接著高優先順序重要的,接著是常規優先順序不重要的,最後是常規優先順序重要規則。這意味著根據正確的層疊規則屬性出現四次。最後的獲勝。

來總結是:共享樣式物件(全部和部分的內部結構)處理問題在 1 和 3。火狐規則樹也會幫助在正確的順序下應用規則。

3.為簡單匹配控制規則

這裡有幾個樣式規則的來源:

  • CSS 規則,既不是外部樣式也不是元素中樣式。
    p {color: blue}
    複製程式碼
  • 內聯元素屬性如下:
    <p style="color: blue" />
    複製程式碼
  • HTML 可視屬性(這對映到相關樣式規則)
    <p bgcolor="blue" />
    複製程式碼

最後兩個對元素來說是簡單匹配,因為它自身的樣式屬性和 HTML 屬性可以作為 key 對映到使用的元素。

注意之前的問題 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 表和標籤表中找到相關規則。剩下的工作只是找出哪些根據鍵提取的規則是真正的匹配了。

比如 div 規則如下:

table div {margin: 5px}
複製程式碼

它會從類表中提取,因為鍵是最右邊的選擇器,但是它不會匹配我們的 div 元素,它沒有 table 祖先。

WebKit 和 火狐都做了這種操作。

4.在正確的層疊順序中應用規則

樣式物件有對應每一個視覺化屬性(所有的 CSS 屬性但更通用)的屬性。如果一個屬性沒有被任何匹配的規則定義,這些屬性可以通過父級元素樣式物件來繼承。其他屬性使用預設值。

當有更多的定義時問題就來了——這時候需要層疊順序來解決這個問題。

1.樣式表層疊規則

樣式屬性的宣告可能會出現在多個樣式表中,或者在一個樣式表中宣告數次。這意味著應用工作的順序是很重要的。這叫做 “層疊” 順序。根據 CSS2 定義,層疊順序是(從低到高):

  1. 瀏覽器宣告
  2. 使用者一般宣告
  3. 創作者一般宣告
  4. 創作者重要宣告
  5. 使用者重要宣告

瀏覽器宣告是不重要的,當使用者只有把宣告標記為 important 時才會覆蓋創作者的內容。同樣順序的宣告會根據定義來排序,然後在根據指定的順序。HTML 可視屬性被轉換成匹配 CSS 宣告。它們被當做低許可權的創作者規則。

2.明確性

選擇器的明確性通過如下 CSS2 規範來定義:

  • 如果宣告來自樣式屬性而不是選擇器屬性(=a),計為 1
  • 計算選擇器中 ID 屬性的數量(=b)
  • 計算選擇器中其他屬性和偽類的數量(=c)
  • 計算選擇器中元素名和偽元素的數量(=d)

連線這四個數字 a-b-c-d(在大基數進位制的數字系統),得到明確性。

基於你需要使用的進製取決於在某個類目中定義的最多的數量。

舉個例子,如果 a = 14 你需要使用 16 進位制。不太可能的情況是 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 */
複製程式碼
3.規則排序

在匹配規則以後,根據層疊規則排序。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;
}
複製程式碼

4.逐步過程

WebKit 使用一個標記,記錄所有最高層加樣式表(包括 @imports)是否載入完畢。如果在 attaching 中沒有完全載入,使用佔位符並且在文件中標記,然後一旦樣式表被夾在會重新計算。

5.佈局

當渲染被建立和新增到樹中後,它還沒有位置和尺寸。計算這些值的過程叫做佈局和重繪(reflow)。

HTML 使用的流基於佈局模型,意味著在大多數情況下一次計算就可以得到幾何值。流之後的元素一般不會影響流之前的元素的幾何性質,所以通過文件佈局可以從左只有,從上到下的執行。有個例外:比如,HTML 表格可能會要求多次遍歷(3.5

座標系統是相對於根框架的。使用上左位置的座標。

佈局是個遞迴過程。它在根節點渲染開始,也就是對應 HTML 文件的 <html> 元素。佈局通過一些或者全部框架層級,為每次要求的渲染計算幾何資訊。

根渲染的位置是 0,0 並且它的尺寸是瀏覽器視窗的可見部分的視窗。

所有的渲染都有 “layout” 和 “reflow” 方法,每個渲染呼叫後代需要佈局的佈局方法。

1.Dirty 位系統

為了對每一次小改變不做充分的佈局,瀏覽器使用 “dirty bit” 系統。改變或者給它自己和後代新增標記的渲染視為 “dirty”:需要佈局。

有兩種標記:“dirty” 和 “children are dirty”, 這意味著儘管渲染自己是合適的,它至少有一個子代需要佈局。

2.全域性和增量佈局

佈局在整個渲染樹上能被觸發——這是“全域性”佈局。它能作為以下結果發生:

  1. 影響所有渲染的全域性樣式,像是字型大小改變。
  2. 螢幕改變的結果。

佈局是增量的,只有髒渲染會佈局(這回引起一些額外佈局的損失)。

增量佈局當渲染是髒的時候觸發(非同步的)。舉個例子,在從網路的額外內容被新增到 DOM 樹後新的渲染將被新增到渲染樹。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

增量佈局——只有髒渲染和它的後代佈局

3.非同步和同步佈局

增量佈局是通過非同步完成的。火狐為增量佈局將 “迴流命令”排隊,同時排程者會觸發這些命令批量執行,然後“髒”渲染被佈局。

指令碼請求了樣式順序,像是“offsetHeight”可以同步地觸發增量佈局。

全域性佈局通常被同步觸發。

有時因為一些屬性,比如滾動位置改變,佈局會在初始化佈局之後作為回撥觸發。

4.優化

優化當佈局被 “resize” 觸發時,或者渲染位置改變(不是尺寸),渲染尺寸從快取中獲取並且不會重新計算。

在一些例子中只有一個子樹被修改,並且佈局沒有從根節點開始的話。這地方只會發生本地改變不會影響它的周圍——比如文字被插入進文字域(否則每次敲擊將會從根節點觸釋出局)。

5.佈局過程

佈局通常有以下預設:

  1. 父級渲染決定自身寬度
  2. 父級依次處理後代:
    1. 放置子代渲染(設定 x 和 y)
    2. 呼叫子代佈局,如果需要的話——它們是髒佈局或者我們在在全域性中,或者其他的原因——這會計運算元代高度
  3. 父級使用子代積累的高度和邊距的高度,然後補充自己的高度——這會通過父級渲染的父級使用。
  4. 設定髒位值為 false

火狐使用一個 “state” 物件(nxHTMLReflowState)作為佈局引數(記為“reflow”)。在它們之間這個狀態包括父級寬度。火狐的佈局輸出是 “metrics” 物件(nsHTMLReflowMetrics)。它將包含渲染計算的高度。

6.寬度計算

渲染器的寬度使用包含塊的寬度來計算,渲染樣式 “width” 屬性是 margin 和 border。

比如下面這個 div 的寬度:

<div style="width: 30%"/>
複製程式碼

通過 WebKit 計算可能如下(RenderBox 類 的 calcWidth 方法):

  • 包含的寬度是包含塊可用寬度的最大值或 0.這個可以用寬度在例子中是這樣被計算的內容寬度:

    clientWidth() - paddingLeft() - paddingRight()
    複製程式碼

    客戶端寬度和客戶端高度代表一個包括邊距和滾動調的物件的內部

  • 元素寬度是樣式屬性 “width”。它可以計算成絕對值,通過計算容器寬度的百分比。

  • 水平邊框和補白被新增。

目前這種這是“完美寬度”的計算。現在最小和最大寬度被計算。

如果最佳寬度比最大寬度更大,這個最大寬度將被使用。如果小於最小寬度(最小的不可破壞的單位)那麼最小寬度被使用。

只被快取以防佈局使用,但是寬度不會改變。

7.斷行

當渲染到佈局的中間決定它需要換行時,渲染停止然後擴散需要換行佈局的父級。父級建立額外的渲染,然後在上面呼叫渲染。

6.繪製

在繪製階段,渲染被傳遞,並且渲染的 “paint()” 方法被呼叫用於在螢幕上展示內容。繪製使用 UI 基礎元件。

1.全域性和增量

如同佈局,繪製也是全域性的——整棵樹被繪製——或者增加。在增加繪製中,一些渲染在不影響整顆樹的情況下改變。這個改變的渲染在螢幕上使它的矩形失效。這是因為作業系統把它當做一塊“髒區域”,同時生成了“繪製”事件。作業系統聰明的合併幾個區域變成一個。在 Chrome 中,這比較複雜因為渲染在主過程中有不同的過程。Chrome 某些程度模擬了作業系統的行為。表現層監聽了事件並且代理渲染根部的訊息。樹被傳遞直到相關渲染到達。它將重繪自己(和通常它的子代)。

2.繪製順序

CSS2 定義了繪製過程的順序。實際上這個順序是元素在內容棧的儲存的地方。因為棧渲染從後向前,所以這個順序影響繪製。一個塊的棧順序渲染是:

  1. 背景色
  2. 背景圖片
  3. 邊框
  4. 子代
  5. 外部描線

3.Firefox 展示列表

火狐遍歷渲染樹,然後為繪製矩形構建展示列表。它包含渲染層相關的矩形,在正確的繪製順序下(渲染層的背景和邊框等等)。這種方式樹為一次重繪只會傳遞一次而不是數次——繪製所有的背景和圖片,然後是邊框等等。

火狐通過不新增將被隱藏的元素優化過程,像是元素完全在其他不透明元素下方。

4.WebKit 矩陣儲存

在重繪前,WebKit 儲存舊的矩形作為點陣圖。這樣只會渲染在新舊矩形之間的變化。

7.動態改變

在響應變化時,瀏覽器嘗試最小化的可能的行為。所以改變一個元素的顏色只會引起元素重繪。改變元素的位置會引起元素和它的子代或可能的兄弟節點的佈局和重繪。新增一個節點將引起節點的佈局和重繪。主要的變化,像是增加 “html” 元素的字號,將會引起快取失效,整個樹的重佈局和重繪製。

8.渲染引擎的執行緒

渲染引擎是單執行緒的。幾乎所有的事,除了網路操作,都發生在單執行緒中。在火狐和 Safari 中這是瀏覽器的主執行緒。在 Chrome 中,tab 過程是主執行緒。

網路操作可以通過幾個平行執行緒執行。平行連結數是受限的(2-6 個連結)。

1.事件迴圈

瀏覽器主要執行緒是時間迴圈。這是一個保持過程活動的無限迴圈。它等待事件(像是佈局和繪製事件)然後執行它們。下面是火狐程式碼的主要事件迴圈:

while (!mExiting)
    NS_ProcessNextEvent(thread);
複製程式碼

9.CSS2 可視模型

1.canvas

根據 CSS2 定義,canvas 條款描述 “格式化結構渲染的空間”:是瀏覽器繪製內容的地方。canvas 對每個空間的尺寸是無限的,但是瀏覽器基於視窗的尺寸選擇一個初始化的寬度。

根據 www.w3.org/TR/CSS2/zin…,canvas 是透明的,如果包含其他內容,然而瀏覽器定義一個顏色如果它沒有定義的話。

2.CSS 盒模型

CSS 盒模型描述了一個矩形盒子,它在文件中為元素生成,同時根據視覺化格式模型佈局。

每個盒子有一個內容面積(比如文字和圖片等等),同時可選有間距,邊框,和邊距面積。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:CSS2 盒模型

每個節點生成 0 到 n 和盒子。

所有的元素都有 “display” 屬性,來定義將被生成的盒子型別。比如:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
複製程式碼

預設是 inline,但是瀏覽器樣式表可能設定其他預設。舉個例子:“div” 元素的預設展示是 “block”。

你可以在這裡檢視預設樣式表的例子:www.w3.org/TR/CSS2/sam…

3.定位方案

有三種方案:

  1. 正常:物件根據文件中的位置被放置。意味著它在渲染樹中的位置像在 DOM 樹中的位置,根據盒模型和尺寸來佈局。
  2. 浮動:物件像首先像正常流一樣佈局,然後儘可能移動到最最左或者最右。
  3. 絕對定位:物件放置在渲染樹中,跟 DOM 樹的位置不同。

定位方案通過設定 “position” 屬性和 “float” 屬性。

  • 靜態和相對定位引發正常流
  • 絕對和固定定位引發絕對定位

在靜態定位中,沒有位置被定義,並且使用預設位置。在其他方案中,創作者定義位置用:上下左右。

盒子佈局的方式取決於:

  • 盒子型別
  • 盒子尺寸
  • 位置方案
  • 外部資訊比如圖片大小和螢幕尺寸

4.盒子型別

塊狀盒子:在瀏覽器視窗中有自己的矩形的一種盒子形式。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:塊盒子

內聯盒子:沒有自己的塊,但是內部有內容塊。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:內聯盒子

塊是一個接一個的格式化垂直。內聯是格式化水平。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:塊和內聯格式化

內聯盒子內部有行或者 “line boxes”。行至少與最高的盒子一樣高而且可以比它更高,當盒子對齊在 “baseline”時——意味著底部元素部分對齊另一個元素的底部。如果容器寬度不夠,內聯會換行。這個通常發生在段落中。

5.定位

1.相對

相對定位-像通常一樣定位,然後根據變化移動。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:相對定位

2.浮動

一個浮動盒子漂移到行的左側或者右側。這個有趣的特性會讓其他盒子圍繞著它。

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

看起來像這樣:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:浮動

3.絕對和固定

佈局精確定義忽略正常流。元素不參與正常流。這個尺寸相對於容器。在固定定位中,容器是視窗。

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:固定位置

注意:固定盒子在文件滾動的時候不會移動。

6.表現層

這個通過 CSS 的 z-index 屬性定義。它代表沿 “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>
複製程式碼

結果是:

【譯】瀏覽器如何工作:在現代web瀏覽器場景的之下

圖例:固定定位

儘管紅色 div 在構建上高於綠色的,它可能在常規流之前,它的 z-index 屬性 更高,所以在根盒子持有的棧中更向前。

10.資料

1.瀏覽器結構

  1. Grosskurth, Alan. Web 瀏覽器的引用結構
  2. Gupta, Vineet. 瀏覽器如何工作——第一部分——結構

2.解析

  1. Aho, Sethi, Ullman, 編譯:規則,技術和工具(又叫做 “龍書”)Addison-Wesley, 1986
  2. Rick Jelliffe. 粗體之美:HTML 5 的兩種草案

3.火狐

  1. L. David Baron, HTML 和 CSS 特性:寫給 Web 開發者的佈局引擎的內部
  2. L. David Baron, Mozilla 的佈局引擎
  3. L. David Baron Mozilla 樣式系統文件
  4. Chris Waterson, HTML 迴流筆記
  5. Chris Waterson, Geoko 概述
  6. Alexander Larsson,HTML HTTP 請求週期

4.WebKit

5. W3C 規範

  1. HTML 4.01 規範
  2. W3C HTML5 規範
  3. 層疊樣式表 2 級版本 規範
    pic

6. 瀏覽器構建說明

  1. 火狐 developer.mozilla.org/en/Build_Do…
  2. WebKit webkit.org/building/bu…

相關文章