瀏覽器內部工作原理

icysoul發表於2013-02-18

  目錄

  一、介紹
  二、渲染引擎
  三、解析與DOM樹構建
  四、渲染樹構建
  五、佈局
  六、繪製
  七、動態變化
  八、渲染引擎的執行緒
  九、CSS2可視模型

  一、介紹

  瀏覽器可以被認為是使用最廣泛的軟體,本文將介紹瀏覽器的工作原理,我們將看到,從你在位址列輸入google.com到你看到google主頁過程中都發生了什麼。

  將討論的瀏覽器

  今天,有五種主流瀏覽器——IE、Firefox、Safari、Chrome及Opera。

  本文將基於一些開源瀏覽器的例子——Firefox、Chrome及Safari,Safari是部分開源的。

  根據W3C(World Wide Web Consortium全球資訊網聯盟)的瀏覽器統計資料,當前(2011年5月),Firefox、Safari及Chrome的市場佔有率綜合已接近60%。(原文為2009年10月,資料沒有太大變化)因此,可以說開源瀏覽器已經佔據了瀏覽器市場的半壁江山。

  瀏覽器的主要功能

  瀏覽器的主要功能是將使用者選擇的web資源呈現出來,它需要從伺服器請求資源,並將其顯示在瀏覽器視窗中,資源的格式通常是HTML,也包括PDF、image及其他格式。使用者用URI(Uniform Resource Identifier統一資源識別符號)來指定所請求資源的位置,在網路一章有更多討論。

  HTML和CSS規範中規定了瀏覽器解釋html文件的方式,由W3C組織對這些規範進行維護,W3C是負責制定web標準的組織。

  HTML規範的最新版本是HTML4(http://www.w3.org/TR/html401/),HTML5還在制定中(譯註:兩年前),最新的CSS規範版本是2(http://www.w3.org/TR/CSS2),CSS3也還正在制定中(譯註:同樣兩年前)。

  這些年來,瀏覽器廠商紛紛開發自己的擴充套件,對規範的遵循並不完善,這為web開發者帶來了嚴重的相容性問題。

  但是,瀏覽器的使用者介面則差不多,常見的使用者介面元素包括:

  • 用來輸入URI的位址列
  • 前進、後退按鈕
  • 書籤選項
  • 用於重新整理及暫停當前載入文件的重新整理、暫停按鈕
  • 用於到達主頁的主頁按鈕

  奇怪的是,並沒有哪個正式公佈的規範對使用者介面做出規定,這些是多年來各瀏覽器廠商之間相互模仿和不斷改進的結果。

  HTML5並沒有規定瀏覽器必須具有的UI元素,但列出了一些常用元素,包括位址列、狀態列及工具欄。還有一些瀏覽器有自己專有的功能,比如Firefox的下載管理。更多相關內容將在後面討論使用者介面時介紹。

  瀏覽器的主要構成(High Level Structure)

  瀏覽器的主要元件包括:

  1. 使用者介面 - 包括位址列、後退/前進按鈕、書籤目錄等,也就是你所看到的除了用來顯示你所請求頁面的主視窗之外的其他部分。

  2. 瀏覽器引擎 - 用來查詢及操作渲染引擎的介面。

  3. 渲染引擎 - 用來顯示請求的內容,例如,如果請求內容為html,它負責解析html及css,並將解析後的結果顯示出來。

  4. 網路 - 用來完成網路呼叫,例如http請求,它具有平臺無關的介面,可以在不同平臺上工作。

  5. UI後端 - 用來繪製類似組合選擇框及對話方塊等基本元件,具有不特定於某個平臺的通用介面,底層使用作業系統的使用者介面。

  6. JS直譯器 - 用來解釋執行JS程式碼。

  7. 資料儲存 - 屬於持久層,瀏覽器需要在硬碟中儲存類似cookie的各種資料,HTML5定義了web database技術,這是一種輕量級完整的客戶端儲存技術

圖1:瀏覽器主要元件

  需要注意的是,不同於大部分瀏覽器,Chrome為每個Tab分配了各自的渲染引擎例項,每個Tab就是一個獨立的程式。

  對於構成瀏覽器的這些元件,後面會逐一詳細討論。

  二、渲染引擎(The rendering engine)

  渲染引擎的職責就是渲染,即在瀏覽器視窗中顯示所請求的內容。

  預設情況下,渲染引擎可以顯示html、xml文件及圖片,它也可以藉助外掛(一種瀏覽器擴充套件)顯示其他型別資料,例如使用PDF閱讀器外掛,可以顯示PDF格式,將由專門一章講解外掛及擴充套件,這裡只討論渲染引擎最主要的用途——顯示應用了CSS之後的html及圖片。

  渲染引擎簡介

  本文所討論的瀏覽器——Firefox、Chrome和Safari是基於兩種渲染引擎構建的,Firefox使用Geoko——Mozilla自主研發的渲染引擎,Safari和Chrome都使用webkit。

  Webkit是一款開源渲染引擎,它本來是為Linux平臺研發的,後來由Apple移植到Mac及Windows上,相關內容請參考http://webkit.org

  渲染主流程(The main flow)

  渲染引擎首先通過網路獲得所請求文件的內容,通常以8K分塊的方式完成。

  下面是渲染引擎在取得內容之後的基本流程:

  解析html以構建dom樹 -> 構建render樹 -> 佈局render樹 -> 繪製render樹

圖2:渲染引擎基本流程

  渲染引擎開始解析html,並將標籤轉化為內容樹中的dom節點。接著,它解析外部CSS檔案及style標籤中的樣式資訊。這些樣式資訊以及html中的可見性指令將被用來構建另一棵樹——render樹。

  Render樹由一些包含有顏色和大小等屬性的矩形組成,它們將被按照正確的順序顯示到螢幕上。

  Render樹構建好了之後,將會執行佈局過程,它將確定每個節點在螢幕上的確切座標。再下一步就是繪製,即遍歷render樹,並使用UI後端層繪製每個節點。

  值得注意的是,這個過程是逐步完成的,為了更好的使用者體驗,渲染引擎將會盡可能早的將內容呈現到螢幕上,並不會等到所有的html都解析完成之後再去構建和佈局render樹。它是解析完一部分內容就顯示一部分內容,同時,可能還在通過網路下載其餘內容。

圖3:webkit主流程

圖4:Mozilla的Geoko渲染引擎主流程

  從圖3和4中可以看出,儘管webkit和Gecko使用的術語稍有不同,他們的主要流程基本相同。Gecko稱可見的格式化元素組成的樹為frame樹,每個元素都是一個frame,webkit則使用render樹這個名詞來命名由渲染物件組成的樹。Webkit中元素的定位稱為佈局,而Gecko中稱為迴流。Webkit稱利用dom節點及樣式資訊去構建render樹的過程為attachment,Gecko在html和dom樹之間附加了一層,這層稱為內容接收器,相當製造dom元素的工廠。下面將討論流程中的各個階段。

  三、解析與DOM樹構建(Parsing and DOM tree construction)

  解析(Parsing-general)

  既然解析是渲染引擎中一個非常重要的過程,我們將稍微深入的研究它。首先簡要介紹一下解析。

  解析一個文件即將其轉換為具有一定意義的結構——編碼可以理解和使用的東西。解析的結果通常是表達文件結構的節點樹,稱為解析樹或語法樹。

  例如,解析“2+3-1”這個表示式,可能返回這樣一棵樹。

圖5:數學表示式樹節點

  文法(Grammars)

  解析基於文件依據的語法規則——文件的語言或格式。每種可被解析的格式必須具有由詞彙及語法規則組成的特定的文法,稱為上下文無關文法。人類語言不具有這一特性,因此不能被一般的解析技術所解析。

  解析器-詞法分析器(Parser-Lexer combination)

  解析可以分為兩個子過程——語法分析及詞法分析

  詞法分析就是將輸入分解為符號,符號是語言的詞彙表——基本有效單元的集合。對於人類語言來說,它相當於我們字典中出現的所有單詞。

  語法分析指對語言應用語法規則。

  解析器一般將工作分配給兩個元件——詞法分析器(有時也叫分詞器)負責將輸入分解為合法的符號,解析器則根據語言的語法規則分析文件結構,從而構建解析樹,詞法分析器知道怎麼跳過空白和換行之類的無關字元。

圖6:從源文件到解析樹

  解析過程是迭代的,解析器從詞法分析器處取到一個新的符號,並試著用這個符號匹配一條語法規則,如果匹配了一條規則,這個符號對應的節點將被新增到解析樹上,然後解析器請求另一個符號。如果沒有匹配到規則,解析器將在內部儲存該符號,並從詞法分析器取下一個符號,直到所有內部儲存的符號能夠匹配一項語法規則。如果最終沒有找到匹配的規則,解析器將丟擲一個異常,這意味著文件無效或是包含語法錯誤。

  轉換(Translation)

  很多時候,解析樹並不是最終結果。解析一般在轉換中使用——將輸入文件轉換為另一種格式。編譯就是個例子,編譯器在將一段原始碼編譯為機器碼的時候,先將原始碼解析為解析樹,然後將該樹轉換為一個機器碼文件。

圖7:編譯流程

  解析例項Parsing example

  圖5中,我們從一個數學表示式構建了一個解析樹,這裡定義一個簡單的數學語言來看下解析過程。

  詞彙表:我們的語言包括整數、加號及減號。

  語法:

  1. 該語言的語法基本單元包括表示式、term及操作符

  2. 該語言可以包括多個表示式

  3. 一個表示式定義為兩個term通過一個操作符連線

  4. 操作符可以是加號或減號

  5. term可以是一個整數或一個表示式

  現在來分析一下“2+3-1”這個輸入

  第一個匹配規則的子字串是“2”,根據規則5,它是一個term,第二個匹配的是“2+3”,它符合第2條規則——一個操作符連線兩個term,下一次匹配發生在輸入的結束處。“2+3-1”是一個表示式,因為我們已經知道“2+3”是一個term,所以我們有了一個term緊跟著一個操作符及另一個term。“2++”將不會匹配任何規則,因此是一個無效輸入。

  詞彙表及語法的定義

  詞彙表通常利用正規表示式來定義。

  例如上面的語言可以定義為:

  INTEGER:0|[1-9][0-9]*

  PLUS:+

  MINUS:-

  正如看到的,這裡用正規表示式定義整數。

  語法通常用BNF格式定義,我們的語言可以定義為:

  expression := term operation term

  operation := PLUS | MINUS

  term := INTEGER | expression

  如果一個語言的文法是上下文無關的,則它可以用正則解析器來解析。對上下文無關文法的一個直觀的定義是,該文法可以用BNF來完整的表達。可檢視http://en.wikipedia.org/wiki/Context-free_grammar

  解析器型別(Types of parsers)

  有兩種基本的解析器——自頂向下解析及自底向上解析。比較直觀的解釋是,自頂向下解析,檢視語法的最高層結構並試著匹配其中一個;自底向上解析則從輸入開始,逐步將其轉換為語法規則,從底層規則開始直到匹配高層規則。

  來看一下這兩種解析器如何解析上面的例子:

  自頂向下解析器從最高層規則開始——它先識別出“2+3“,將其視為一個表示式,然後識別出”2+3-1“為一個表示式(識別表示式的過程中匹配了其他規則,但出發點是最高層規則)。

  自底向上解析會掃描輸入直到匹配了一條規則,然後用該規則取代匹配的輸入,直到解析完所有輸入。部分匹配的表示式被放置在解析堆疊中。

Stack

Input

 

2 + 3 – 1

term

+ 3 - 1

term operation

3 – 1

expression

- 1

expression operation

1

expression

 

  自底向上解析器稱為shift reduce解析器,因為輸入向右移動(想象一個指標首先指向輸入開始處,並向右移動),並逐漸簡化為語法規則。

  自動化解析(Generating parsers automatically)

  解析器生成器這個工具可以自動生成解析器,只需要指定語言的文法——詞彙表及語法規則,它就可以生成一個解析器。建立一個解析器需要對解析有深入的理解,而且手動的建立一個由較好效能的解析器並不容易,所以解析生成器很有用。Webkit使用兩個知名的解析生成器——用於建立語法分析器的Flex及建立解析器的Bison(你可能接觸過Lex和Yacc)。Flex的輸入是一個包含了符號定義的正規表示式,Bison的輸入是用BNF格式表示的語法規則。

  HTML解析器(HTML Parser)

  HTML解析器的工作是將html標識解析為解析樹。

  HTML文法定義(The HTML grammar definition)

  W3C組織制定規範定義了HTML的詞彙表和語法。

  非上下文無關文法(Not a context free grammar)

  正如在解析簡介中提到的,上下文無關文法的語法可以用類似BNF的格式來定義。

  不幸的是,所有的傳統解析方式都不適用於html(當然我提出它們並不只是因為好玩,它們將用來解析css和js),html不能簡單的用解析所需的上下文無關文法來定義。

  Html有一個正式的格式定義——DTD(Document Type Definition文件型別定義)——但它並不是上下文無關文法,html更接近於xml,現在有很多可用的xml解析器,html有個xml的變體——xhtml,它們間的不同在於,html更寬容,它允許忽略一些特定標籤,有時可以省略開始或結束標籤。總的來說,它是一種soft語法,不像xml呆板、固執。

  顯然,這個看起來很小的差異卻帶來了很大的不同。一方面,這是html流行的原因——它的寬容使web開發人員的工作更加輕鬆,但另一方面,這也使很難去寫一個格式化的文法。所以,html的解析並不簡單,它既不能用傳統的解析器解析,也不能用xml解析器解析。

  HTML DTD

  Html適用DTD格式進行定義,這一格式是用於定義SGML家族的語言,包括了對所有允許元素及它們的屬性和層次關係的定義。正如前面提到的,html DTD並沒有生成一種上下文無關文法。

  DTD有一些變種,標準模式只遵守規範,而其他模式則包含了對瀏覽器過去所使用標籤的支援,這麼做是為了相容以前內容。最新的標準DTD在http://www.w3.org/TR/html4/strict.dtd

  DOM

  輸出的樹,也就是解析樹,是由DOM元素及屬性節點組成的。DOM是文件物件模型的縮寫,它是html文件的物件表示,作為html元素的外部介面供js等呼叫。

  樹的根是“document”物件。

  DOM和標籤基本是一一對應的關係,例如,如下的標籤:

<html>
<body>
<p>
Hello DOM
</p>
<div><img src=”example.png” /></div>
</body>
</html>

  將會被轉換為下面的DOM樹:

圖8:示例標籤對應的DOM樹

  和html一樣,DOM的規範也是由W3C組織制定的。訪問http://www.w3.org/DOM/DOMTR,這是使用文件的一般規範。一個模型描述一種特定的html元素,可以在http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.htm檢視html定義。

  這裡所謂的樹包含了DOM節點是說樹是由實現了DOM介面的元素構建而成的,瀏覽器使用已被瀏覽器內部使用的其他屬性的具體實現。

  解析演算法(The parsing algorithm)

  正如前面章節中討論的,hmtl不能被一般的自頂向下或自底向上的解析器所解析。

  原因是:

  1. 這門語言本身的寬容特性

  2. 瀏覽器對一些常見的非法html有容錯機制

  3. 解析過程是往復的,通常原始碼不會在解析過程中發生改變,但在html中,指令碼標籤包含的“document.write”可能新增標籤,這說明在解析過程中實際上修改了輸入。

  不能使用正則解析技術,瀏覽器為html定製了專屬的解析器。

  Html5規範中描述了這個解析演算法,演算法包括兩個階段——符號化及構建樹。

  符號化是詞法分析的過程,將輸入解析為符號,html的符號包括開始標籤、結束標籤、屬性名及屬性值。

  符號識別器識別出符號後,將其傳遞給樹構建器,並讀取下一個字元,以識別下一個符號,這樣直到處理完所有輸入。

圖9:HTML解析流程

  符號識別演算法(The tokenization algorithm)

  演算法輸出html符號,該演算法用狀態機表示。每次讀取輸入流中的一個或多個字元,並根據這些字元轉移到下一個狀態,當前的符號狀態及構建樹狀態共同影響結果,這意味著,讀取同樣的字元,可能因為當前狀態的不同,得到不同的結果以進入下一個正確的狀態。

  這個演算法很複雜,這裡用一個簡單的例子來解釋這個原理。

  基本示例——符號化下面的html:

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

  初始狀態為“Data State”,當遇到“<”字元,狀態變為“Tag open state”,讀取一個a-z的字元將產生一個開始標籤符號,狀態相應變為“Tag name state”,一直保持這個狀態直到讀取到“>”,每個字元都附加到這個符號名上,例子中建立的是一個html符號。

  當讀取到“>”,當前的符號就完成了,此時,狀態回到“Data state”,“<body>”重複這一處理過程。到這裡,html和body標籤都識別出來了。現在,回到“Data state”,讀取“Hello world”中的字元“H”將建立並識別出一個字元符號,這裡會為“Hello world”中的每個字元生成一個字元符號。

  這樣直到遇到“</body>”中的“<”。現在,又回到了“Tag open state”,讀取下一個字元“/”將建立一個閉合標籤符號,並且狀態轉移到“Tag name state”,還是保持這一狀態,直到遇到“>”。然後,產生一個新的標籤符號並回到“Data state”。後面的“</html>”將和“</body>”一樣處理。

圖10:符號化示例輸入

  樹的構建演算法(Tree construction algorithm)

  在樹的構建階段,將修改以Document為根的DOM樹,將元素附加到樹上。每個由符號識別器識別生成的節點將會被樹構造器進行處理,規範中定義了每個符號相對應的Dom元素,對應的Dom元素將會被建立。這些元素除了會被新增到Dom樹上,還將被新增到開放元素堆疊中。這個堆疊用來糾正巢狀的未匹配和未閉合標籤,這個演算法也是用狀態機來描述,所有的狀態採用插入模式。

  來看一下示例中樹的建立過程:

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

  構建樹這一階段的輸入是符號識別階段生成的符號序列。

  首先是“initial mode”,接收到html符號後將轉換為“before html”模式,在這個模式中對這個符號進行再處理。此時,建立了一個HTMLHtmlElement元素,並將其附加到根Document物件上。

  狀態此時變為“before head”,接收到body符號時,即使這裡沒有head符號,也將自動建立一個HTMLHeadElement元素並附加到樹上。

  現在,轉到“in head”模式,然後是“after head”。到這裡,body符號會被再次處理,將建立一個HTMLBodyElement並插入到樹中,同時,轉移到“in body”模式。

  然後,接收到字串“Hello world”的字元符號,第一個字元將導致建立並插入一個text節點,其他字元將附加到該節點。

  接收到body結束符號時,轉移到“after body”模式,接著接收到html結束符號,這個符號意味著轉移到了“after after body”模式,當接收到檔案結束符時,整個解析過程結束。

圖11:示例html樹的構建過程

  解析結束時的處理(Action when the parsing is finished)

  在這個階段,瀏覽器將文件標記為可互動的,並開始解析處於延時模式中的指令碼——這些指令碼在文件解析後執行。

  文件狀態將被設定為完成,同時觸發一個load事件。

  Html5規範中有符號化及構建樹的完整演算法(http://www.w3.org/TR/html5/syntax.html#html-parser)。

  瀏覽器容錯(Browsers error tolerance)

  你從來不會在一個html頁面上看到“無效語法”這樣的錯誤,瀏覽器修復了無效內容並繼續工作。

  以下面這段html為例:

<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>

  這段html違反了很多規則(mytag不是合法的標籤,p及div錯誤的巢狀等等),但是瀏覽器仍然可以沒有任何怨言的繼續顯示,它在解析的過程中修復了html作者的錯誤。

  瀏覽器都具有錯誤處理的能力,但是,另人驚訝的是,這並不是html最新規範的內容,就像書籤及前進後退按鈕一樣,它只是瀏覽器長期發展的結果。一些比較知名的非法html結構,在許多站點中出現過,瀏覽器都試著以一種和其他瀏覽器一致的方式去修復。

  Html5規範定義了這方面的需求,webkit在html解析類開始部分的註釋中做了很好的總結。

  解析器將符號化的輸入解析為文件並建立文件,但不幸的是,我們必須處理很多沒有很好格式化的html文件,至少要小心下面幾種錯誤情況。

  1. 在未閉合的標籤中新增明確禁止的元素。這種情況下,應該先將前一標籤閉合

  2. 不能直接新增元素。有些人在寫文件的時候會忘了中間一些標籤(或者中間標籤是可選的),比如HTML HEAD BODY 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;
}

  Note -這裡的錯誤處理在內部進行,使用者看不到。

  迷路的表格

  這指一個表格巢狀在另一個表格中,但不在它的某個單元格內。

  比如下面這個例子:

<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是一個由巢狀層次的站點的例子,最多隻允許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容錯程式碼的範例,否則還是寫格式良好的html吧。

  CSS解析(CSS parsing)

  還記得簡介中提到的解析的概念嗎,不同於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”是識別器的縮寫,相當於一個class名,“name”是一個元素id(用“#”引用)。

  語法用BNF進行描述:

ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator selector ] ]
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
說明:一個規則集合有這樣的結構
div.error , a.error {
color:red;
font-weight:bold;
}
div.error和a.error時選擇器,大括號中的內容包含了這條規則集合中的規則,這個結構在下面的定義中正式的定義了:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;

  這說明,一個規則集合具有一個或是可選個數的多個選擇器,這些選擇器以逗號和空格(S表示空格)進行分隔。每個規則集合包含大括號及大括號中的一條或多條以分號隔開的宣告。宣告和選擇器在後面進行定義。

  Webkit CSS解析器(Webkit CSS parser)

  Webkit使用Flex和Bison解析生成器從CSS語法檔案中自動生成解析器。回憶一下解析器的介紹,Bison建立一個自底向上的解析器,Firefox使用自頂向下解析器。它們都是將每個css檔案解析為樣式表物件,每個物件包含css規則,css規則物件包含選擇器和宣告物件,以及其他一些符合css語法的物件。

圖12:解析css

  處理指令碼及樣式表的順序(The order of processing scripts and style sheets)

  指令碼

  web的模式是同步的,開發者希望解析到一個script標籤時立即解析執行指令碼,並阻塞文件的解析直到指令碼執行完。如果指令碼是外引的,則網路必須先請求到這個資源——這個過程也是同步的,會阻塞文件的解析直到資源被請求到。這個模式保持了很多年,並且在html4及html5中都特別指定了。開發者可以將指令碼標識為defer,以使其不阻塞文件解析,並在文件解析結束後執行。Html5增加了標記指令碼為非同步的選項,以使指令碼的解析執行使用另一個執行緒。

  預解析(Speculative parsing)

  Webkit和Firefox都做了這個優化,當執行指令碼時,另一個執行緒解析剩下的文件,並載入後面需要通過網路載入的資源。這種方式可以使資源並行載入從而使整體速度更快。需要注意的是,預解析並不改變Dom樹,它將這個工作留給主解析過程,自己只解析外部資源的引用,比如外部指令碼、樣式表及圖片。

  樣式表(Style sheets)

  樣式表採用另一種不同的模式。理論上,既然樣式表不改變Dom樹,也就沒有必要停下文件的解析等待它們,然而,存在一個問題,指令碼可能在文件的解析過程中請求樣式資訊,如果樣式還沒有載入和解析,指令碼將得到錯誤的值,顯然這將會導致很多問題,這看起來是個邊緣情況,但確實很常見。Firefox在存在樣式表還在載入和解析時阻塞所有的指令碼,而Chrome只在當指令碼試圖訪問某些可能被未載入的樣式表所影響的特定的樣式屬性時才阻塞這些指令碼。

  四、渲染樹構建(Render tree construction)

  當Dom樹構建完成時,瀏覽器開始構建另一棵樹——渲染樹。渲染樹由元素顯示序列中的可見元素組成,它是文件的視覺化表示,構建這棵樹是為了以正確的順序繪製文件內容。

  Firefox將渲染樹中的元素稱為frames,WebKit則用renderer或渲染物件來描述這些元素。

  一個渲染物件知道怎麼佈局及繪製自己及它的children。

  RenderObject是Webkit的渲染物件基類,它的定義如下:

class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node;//the DOM node
RenderStyle* style;// the computed style
RenderLayer* containgLayer; //the containing z-index layer
}

  每個渲染物件用一個和該節點的css盒模型相對應的矩形區域來表示,正如css2所描述的那樣,它包含諸如寬、高和位置之類的幾何資訊。盒模型的型別受該節點相關的display樣式屬性的影響(參考樣式計算章節)。下面的webkit程式碼說明了如何根據display屬性決定某個節點建立何種型別的渲染物件。

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樹的關係(The render tree relation to the DOM tree)

  渲染物件和Dom元素相對應,但這種對應關係不是一對一的,不可見的Dom元素不會被插入渲染樹,例如head元素。另外,display屬性為none的元素也不會在渲染樹中出現(visibility屬性為hidden的元素將出現在渲染樹中)。

  還有一些Dom元素對應幾個可見物件,它們一般是一些具有複雜結構的元素,無法用一個矩形來描述。例如,select元素有三個渲染物件——一個顯示區域、一個下拉選單及一個按鈕。同樣,當文字因為寬度不夠而折行時,新行將作為額外的渲染元素被新增。另一個多個渲染物件的例子是不規範的html,根據css規範,一個行內元素只能僅包含行內元素或僅包含塊狀元素,在存在混合內容時,將會建立匿名的塊狀渲染物件包裹住行內元素。

  一些渲染物件和所對應的Dom節點不在樹上相同的位置,例如,浮動和絕對定位的元素在文字流之外,在兩棵樹上的位置不同,渲染樹上標識出真實的結構,並用一個佔位結構標識出它們原來的位置。

圖13:渲染樹及對應的Dom樹

  建立樹的流程(The flow of constructing the tree)

  Firefox中,表述為一個監聽Dom更新的監聽器,將frame的建立委派給Frame Constructor,這個構建器計算樣式(參看樣式計算)並建立一個frame。

  Webkit中,計算樣式並生成渲染物件的過程稱為attachment,每個Dom節點有一個attach方法,attachment的過程是同步的,呼叫新節點的attach方法將節點插入到Dom樹中。

  處理html和body標籤將構建渲染樹的根,這個根渲染物件對應被css規範稱為containing block的元素——包含了其他所有塊元素的頂級塊元素。它的大小就是viewport——瀏覽器視窗的顯示區域,Firefox稱它為viewPortFrame,webkit稱為RenderView,這個就是文件所指向的渲染物件,樹中其他的部分都將作為一個插入的Dom節點被建立。

  樣式計算(Style Computation)

  建立渲染樹需要計算出每個渲染物件的可視屬性,這可以通過計算每個元素的樣式屬性得到。

  樣式包括各種來源的樣式表,行內樣式元素及html中的視覺化屬性(例如bgcolor),視覺化屬性轉化為css樣式屬性。

  樣式表來源於瀏覽器預設樣式表,及頁面作者和使用者提供的樣式表——有些樣式是瀏覽器使用者提供的(瀏覽器允許使用者定義喜歡的樣式,例如,在Firefox中,可以通過在Firefox Profile目錄下放置樣式表實現)。

  計算樣式的一些困難:

  1. 樣式資料是非常大的結構,儲存大量的樣式屬性會帶來記憶體問題。

  2. 如果不進行優化,找到每個元素匹配的規則會導致效能問題,為每個元素查詢匹配的規則都需要遍歷整個規則表,這個過程有很大的工作量。選擇符可能有複雜的結構,匹配過程如果沿著一條開始看似正確,後來卻被證明是無用的路徑,則必須去嘗試另一條路徑。

  例如,下面這個複雜選擇符

  div div div div{…}

  這意味著規則應用到三個div的後代div元素,選擇樹上一條特定的路徑去檢查,這可能需要遍歷節點樹,最後卻發現它只是兩個div的後代,並不使用該規則,然後則需要沿著另一條路徑去嘗試

  3. 應用規則涉及非常複雜的級聯,它們定義了規則的層次

  我們來看一下瀏覽器如何處理這些問題:

  共享樣式資料(Sharing style data)

  WebkKit節點引用樣式物件(渲染樣式),某些情況下,這些物件可以被節點間共享,這些節點需要是兄弟或是表兄弟節點,並且:

  1. 這些元素必須處於相同的滑鼠狀態(比如不能一個處於hover,而另一個不是)

  2. 不能有元素具有id

  3. 標籤名必須匹配

  4. class屬性必須匹配

  5. 對應的屬性必須相同

  6. 連結狀態必須匹配

  7. 焦點狀態必須匹配

  8. 不能有元素被屬性選擇器影響

  9. 元素不能有行內樣式屬性

  10. 不能有生效的兄弟選擇器,webcore在任何兄弟選擇器相遇時只是簡單的丟擲一個全域性轉換,並且在它們顯示時使整個文件的樣式共享失效,這些包括+選擇器和類似:first-child和:last-child這樣的選擇器。

  Firefox規則樹(Firefox rule tree)

  Firefox用兩個樹用來簡化樣式計算-規則樹和樣式上下文樹,WebKit也有樣式物件,但它們並沒有儲存在類似樣式上下文樹這樣的樹中,只是由Dom節點指向其相關的樣式。

圖14:Firefox樣式上下文樹

  樣式上下文包含最終值,這些值是通過以正確順序應用所有匹配的規則,並將它們由邏輯值轉換為具體的值,例如,如果邏輯值為螢幕的百分比,則通過計算將其轉化為絕對單位。樣式樹的使用確實很巧妙,它使得在節點中共享的這些值不需要被多次計算,同時也節省了儲存空間。

  所有匹配的規則都儲存在規則樹中,一條路徑中的底層節點擁有最高的優先順序,這棵樹包含了所找到的所有規則匹配的路徑(譯註:可以取巧理解為每條路徑對應一個節點,路徑上包含了該節點所匹配的所有規則)。規則樹並不是一開始就為所有節點進行計算,而是在某個節點需要計算樣式時,才進行相應的計算並將計算後的路徑新增到樹中。

  我們將樹上的路徑看成辭典中的單詞,假如已經計算出瞭如下的規則樹:

  假如需要為內容樹中的另一個節點匹配規則,現在知道匹配的規則(以正確的順序)為B-E-I,因為我們已經計算出了路徑A-B-E-I-L,所以樹上已經存在了這條路徑,剩下的工作就很少了。

  現在來看一下樹如何儲存。

  結構化

  樣式上下文按結構劃分,這些結構包括類似border或color這樣的特定分類的樣式資訊。一個結構中的所有特性不是繼承的就是非繼承的,對繼承的特性,除非元素自身有定義,否則就從它的parent繼承。非繼承的特性(稱為reset特性)如果沒有定義,則使用預設的值。

  樣式上下文樹快取完整的結構(包括計算後的值),這樣,如果底層節點沒有為一個結構提供定義,則使用上層節點快取的結構。

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

  當為一個特定的元素計算樣式時,首先計算出規則樹中的一條路徑,或是使用已經存在的一條,然後使用路徑中的規則去填充新的樣式上下文,從樣式的底層節點開始,它具有最高優先順序(通常是最特定的選擇器),遍歷規則樹,直到填滿結構。如果在那個規則節點沒有定義所需的結構規則,則沿著路徑向上,直到找到該結構規則。

  如果最終沒有找到該結構的任何規則定義,那麼如果這個結構是繼承型的,則找到其在內容樹中的parent的結構,這種情況下,我們也成功的共享了結構;如果這個結構是reset型的,則使用預設的值。

  如果特定的節點新增了值,那麼需要做一些額外的計算以將其轉換為實際值,然後在樹上的節點快取該值,使它的children可以使用。

  當一個元素和它的一個兄弟元素指向同一個樹節點時,完整的樣式上下文可以被它們共享。

  來看一個例子:假設有下面這段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"> verybigerror</span>
error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>

  以及下面這些規則

1.div {margin:5px;color:black}
2..err {color:red}
3..big {margin-top:3px}
4.div span {margin-bottom:4px}
5.#div1 {color:blue}
6.#div2 {color:green}

  簡化下問題,我們只填充兩個結構——color和margin,color結構只包含一個成員-顏色,margin結構包含四邊。

  生成的規則樹如下(節點名:指向的規則)

  上下文樹如下(節點名:指向的規則節點)

  假設我們解析html,遇到第二個div標籤,我們需要為這個節點建立樣式上下文,並填充它的樣式結構。

  我們進行規則匹配,找到這個div匹配的規則為1、2、6,我們發現規則樹上已經存在了一條我們可以使用的路徑1、2,我們只需為規則6新增一個節點新增到下面(就是規則樹中的F)。

  然後建立一個樣式上下文並將其放到上下文樹中,新的樣式上下文將指向規則樹中的節點F。

  現在我們需要填充這個樣式上下文,先從填充margin結構開始,既然最後一個規則節點沒有新增margin結構,沿著路徑向上,直到找到快取的前面插入節點計算出的結構,我們發現B是最近的指定margin值的節點。因為已經有了color結構的定義,所以不能使用快取的結構,既然color只有一個屬性,也就不需要沿著路徑向上填充其他屬性。計算出最終值(將字串轉換為RGB等),並快取計算後的結構。

  第二個span元素更簡單,進行規則匹配後發現它指向規則G,和前一個span一樣,既然有兄弟節點指向同一個節點,就可以共享完整的樣式上下文,只需指向前一個span的上下文。

  因為結構中包含繼承自parent的規則,上下文樹做了快取(color特性是繼承來的,但Firefox將其視為reset並在規則樹中快取)。

  例如,如果我們為一個paragraph的文字新增規則:

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

  那麼這個p在內容樹中的子節點div,會共享和它parent一樣的font結構,這種情況發生在沒有為這個div指定font規則時。

  Webkit中,並沒有規則樹,匹配的宣告會被遍歷四次,先是應用非important的高優先順序屬性(之所以先應用這些屬性,是因為其他的依賴於它們-比如display),其次是高優先順序important的,接著是一般優先順序非important的,最後是一般優先順序important的規則。這樣,出現多次的屬性將被按照正確的級聯順序進行處理,最後一個生效。

  總結一下,共享樣式物件(結構中完整或部分內容)解決了問題1和3,Firefox的規則樹幫助以正確的順序應用規則。

  對規則進行處理以簡化匹配過程

  樣式規則有幾個來源:

  • 外部樣式表或style標籤內的css規則
  • 行內樣式屬性
  • html視覺化屬性(對映為相應的樣式規則)

  後面兩個很容易匹配到元素,因為它們所擁有的樣式屬性和html屬性可以將元素作為key進行對映。

  就像前面問題2所提到的,css的規則匹配可能很狡猾,為了解決這個問題,可以先對規則進行處理,以使其更容易被訪問。

  解析完樣式表之後,規則會根據選擇符新增一些hash對映,對映可以是根據id、class、標籤名或是任何不屬於這些分類的綜合對映。如果選擇符為id,規則將被新增到id對映,如果是class,則被新增到class對映,等等。

  這個處理是匹配規則更容易,不需要檢視每個宣告,我們能從對映中找到一個元素的相關規則,這個優化使在進行規則匹配時減少了95+%的工作量。

  來看下面的樣式規則:

p.error {color:red}
#messageDiv {height:50px}
div {margin:5px}

  第一條規則將被插入class對映,第二條插入id對映,第三條是標籤對映。

  下面這個html片段:

<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>

  我們首先找到p元素對應的規則,class對映將包含一個“error”的key,找到p.error的規則,div在id對映和標籤對映中都有相關的規則,剩下的工作就是找出這些由key對應的規則中哪些確實是正確匹配的。

  例如,如果div的規則是

table div {margin:5px}

  這也是標籤對映產生的,因為key是最右邊的選擇符,但它並不匹配這裡的div元素,因為這裡的div沒有table祖先。

  Webkit和Firefox都會做這個處理。

  以正確的級聯順序應用規則

  樣式物件擁有對應所有可見屬性的屬性,如果特性沒有被任何匹配的規則所定義,那麼一些特性可以從parent的樣式物件中繼承,另外一些使用預設值。

  這個問題的產生是因為存在不止一處的定義,這裡用級聯順序解決這個問題。

  樣式表的級聯順序

  一個樣式屬性的宣告可能在幾個樣式表中出現,或是在一個樣式表中出現多次,因此,應用規則的順序至關重要,這個順序就是級聯順序。根據css2的規範,級聯順序為(從低到高):

  1. 瀏覽器宣告

  2. 使用者宣告

  3. 作者的一般宣告

  4. 作者的important宣告

  5. 使用者important宣告

  瀏覽器宣告是最不重要的,使用者只有在宣告被標記為important時才會覆蓋作者的宣告。具有同等級別的宣告將根據specifity以及它們被定義時的順序進行排序。Html視覺化屬性將被轉換為匹配的css宣告,它們被視為最低優先順序的作者規則。

  Specifity

  Css2規範中定義的選擇符specifity如下:

  • 如果宣告來自style屬性,而不是一個選擇器的規則,則計1,否則計0(=a)
  • 計算選擇器中id屬性的數量(=b)
  • 計算選擇器中class及偽類的數量(=c)
  • 計算選擇器中元素名及偽元素的數量(=d)

  連線a-b-c-d四個數量(用一個大基數的計算系統)將得到specifity。這裡使用的基數由分類中最高的基數定義。例如,如果a為14,可以使用16進位制。不同情況下,a為17時,則需要使用阿拉伯數字17作為基數,這種情況可能在這個選擇符時發生html body div div …(選擇符中有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 */

/* 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;
}

  逐步處理Gradual process

  webkit使用一個標誌位標識所有頂層樣式表都已載入,如果在attch時樣式沒有完全載入,則放置佔位符,並在文件中標記,一旦樣式表完成載入就重新進行計算。

  五、佈局(Layout)

  當渲染物件被建立並新增到樹中,它們並沒有位置和大小,計算這些值的過程稱為layout或reflow。

  Html使用基於流的佈局模型,意味著大部分時間,可以以單一的途徑進行幾何計算。流中靠後的元素並不會影響前面元素的幾何特性,所以佈局可以在文件中從右向左、自上而下的進行。也存在一些例外,比如html tables。

  座標系統相對於根frame,使用top和left座標。

  佈局是一個遞迴的過程,由根渲染物件開始,它對應html文件元素,佈局繼續遞迴的通過一些或所有的frame層級,為每個需要幾何資訊的渲染物件進行計算。

  根渲染物件的位置是0,0,它的大小是viewport-瀏覽器視窗的可見部分。

  所有的渲染物件都有一個layout或reflow方法,每個渲染物件呼叫需要佈局的children的layout方法。

  Dirty bit系統

  為了不因為每個小變化都全部重新佈局,瀏覽器使用一個dirty bit系統,一個渲染物件發生了變化或是被新增了,就標記它及它的children為dirty——需要layout。存在兩個標識——dirty及children are dirty,children are dirty說明即使這個渲染物件可能沒問題,但它至少有一個child需要layout。

  全域性和增量layout

  當layout在整棵渲染樹觸發時,稱為全域性layout,這可能在下面這些情況下發生:

  1. 一個全域性的樣式改變影響所有的渲染物件,比如字號的改變。

  2. 視窗resize。

  layout也可以是增量的,這樣只有標誌為dirty的渲染物件會重新佈局(也將導致一些額外的佈局)。增量layout會在渲染物件dirty時非同步觸發,例如,當網路接收到新的內容並新增到Dom樹後,新的渲染物件會新增到渲染樹中。

圖20:增量layout

  非同步和同步layout

  增量layout的過程是非同步的,Firefox為增量layout生成了reflow佇列,以及一個排程執行這些批處理命令。WebKit也有一個計時器用來執行增量layout-遍歷樹,為dirty狀態的渲染物件重新佈局。

  另外,當指令碼請求樣式資訊時,例如“offsetHeight”,會同步的觸發增量佈局。

  全域性的layout一般都是同步觸發。

  有些時候,layout會被作為一個初始layout之後的回撥,比如滑動條的滑動。

  優化

  當一個layout因為resize或是渲染位置改變(並不是大小改變)而觸發時,渲染物件的大小將會從快取中讀取,而不會重新計算。

  一般情況下,如果只有子樹發生改變,則layout並不從根開始。這種情況發生在,變化發生在元素自身並且不影響它周圍元素,例如,將文字插入文字域(否則,每次擊鍵都將觸發從根開始的重排)。

  layout過程

  layout一般有下面這幾個部分:

  1. parent渲染物件決定它的寬度

  2. parent渲染物件讀取chilidren,並:

    a. 放置child渲染物件(設定它的x和y)

    b. 在需要時(它們當前為dirty或是處於全域性layout或者其他原因)呼叫child渲染物件的layout,這將計算child的高度

    c. parent渲染物件使用child渲染物件的累積高度,以及margin和padding的高度來設定自己的高度-這將被parent渲染物件的parent使用

    d. 將dirty標識設定為false

  Firefox使用一個“state”物件(nsHTMLReflowState)做為引數去佈局(firefox稱為reflow),state包含parent的寬度及其他內容。

  Firefox佈局的輸出是一個“metrics”物件(nsHTMLReflowMetrics)。它包括渲染物件計算出的高度。

  寬度計算

  渲染物件的寬度使用容器的寬度、渲染物件樣式中的寬度及margin、border進行計算。例如,下面這個div的寬度:

  <div />

  webkit中寬度的計算過程是(RenderBox類的calcWidth方法):

  • 容器的寬度是容器的可用寬度和0中的最大值,這裡的可用寬度為:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight代表一個物件內部的不包括border和滑動條的大小
  • 元素的寬度指樣式屬性width的值,它可以通過計算容器的百分比得到一個絕對值
  • 加上水平方向上的border和padding

  到這裡是最佳寬度的計算過程,現在計算寬度的最大值和最小值,如果最佳寬度大於最大寬度則使用最大寬度,如果小於最小寬度則使用最小寬度。最後快取這個值,當需要layout但寬度未改變時使用。

  Line breaking

  當一個渲染物件在佈局過程中需要折行時,則暫停並告訴它的parent它需要折行,parent將建立額外的渲染物件並呼叫它們的layout。

  六、繪製(Painting)

  繪製階段,遍歷渲染樹並呼叫渲染物件的paint方法將它們的內容顯示在螢幕上,繪製使用UI基礎元件,這在UI的章節有更多的介紹。

  全域性和增量

  和佈局一樣,繪製也可以是全域性的——繪製完整的樹——或增量的。在增量的繪製過程中,一些渲染物件以不影響整棵樹的方式改變,改變的渲染物件使其在螢幕上的矩形區域失效,這將導致作業系統將其看作dirty區域,併產生一個paint事件,作業系統很巧妙的處理這個過程,並將多個區域合併為一個。Chrome中,這個過程更復雜些,因為渲染物件在不同的程式中,而不是在主程式中。Chrome在一定程度上模擬作業系統的行為,表現為監聽事件並派發訊息給渲染根,在樹中查詢到相關的渲染物件,重繪這個物件(往往還包括它的children)。

  繪製順序

  css2定義了繪製過程的順序——http://www.w3.org/TR/CSS21/zindex.html。這個就是元素壓入堆疊的順序,這個順序影響著繪製,堆疊從後向前進行繪製。

  一個塊渲染物件的堆疊順序是:

  1. 背景色

  2. 背景圖

  3. border

  4. children

  5. outline

  Firefox顯示列表

  Firefox讀取渲染樹併為繪製的矩形建立一個顯示列表,該列表以正確的繪製順序包含這個矩形相關的渲染物件。

  用這樣的方法,可以使重繪時只需查詢一次樹,而不需要多次查詢——繪製所有的背景、所有的圖片、所有的border等等。

  Firefox優化了這個過程,它不新增會被隱藏的元素,比如元素完全在其他不透明元素下面。

  WebKit矩形儲存

  重繪前,WebKit將舊的矩形儲存為點陣圖,然後只繪製新舊矩形的差集。

  七、動態變化

  瀏覽器總是試著以最小的動作響應一個變化,所以一個元素顏色的變化將只導致該元素的重繪,元素位置的變化將大致元素的佈局和重繪,新增一個Dom節點,也會大致這個元素的佈局和重繪。一些主要的變化,比如增加html元素的字號,將會導致快取失效,從而引起整數的佈局和重繪。

  八、渲染引擎的執行緒

  渲染引擎是單執行緒的,除了網路操作以外,幾乎所有的事情都在單一的執行緒中處理,在Firefox和Safari中,這是瀏覽器的主執行緒,Chrome中這是tab的主執行緒。

  網路操作由幾個並行執行緒執行,並行連線的個數是受限的(通常是2-6個)。

  事件迴圈

  瀏覽器主執行緒是一個事件迴圈,它被設計為無限迴圈以保持執行過程的可用,等待事件(例如layout和paint事件)並執行它們。下面是Firefox的主要事件迴圈程式碼。

while (!mExiting)

NS_ProcessNextEvent(thread);

  九、CSS2可視模型(CSS2 visual module)

  畫布The Canvas

  根據CSS2規範,術語canvas用來描述格式化的結構所渲染的空間——瀏覽器繪製內容的地方。畫布對每個維度空間都是無限大的,但瀏覽器基於viewport的大小選擇了一個初始寬度。

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

  CSS盒模型

  CSS盒模型描述了矩形盒,這些矩形盒是為文件樹中的元素生成的,並根據可視的格式化模型進行佈局。每個box包括內容區域(如圖片、文字等)及可選的四周padding、border和margin區域。

  每個節點生成0-n個這樣的box。

  所有的元素都有一個display屬性,用來決定它們生成box的型別,例如:

  block -生成塊狀box

  inline -生成一個或多個行內box

  none -不生成box

  預設的是inline,但瀏覽器樣式表設定了其他預設值,例如,div元素預設為block。可以訪問http://www.w3.org/TR/CSS2/sample.html檢視更多的預設樣式表示例。

  定位策略Position scheme

  這裡有三種策略:

  1. normal -物件根據它在文件的中位置定位,這意味著它在渲染樹和在Dom樹中位置一致,並根據它的盒模型和大小進行佈局。

  2. float -物件先像普通流一樣佈局,然後儘可能的向左或是向右移動。

  3. absolute -物件在渲染樹中的位置和Dom樹中位置無關。

  static和relative是normal,absolute和fixed屬於absolute。

  在static定位中,不定義位置而使用預設的位置。其他策略中,作者指定位置——top、bottom、left、right。

  Box佈局的方式由這幾項決定:box的型別、box的大小、定位策略及擴充套件資訊(比如圖片大小和螢幕尺寸)。

  Box型別

  Block box:構成一個塊,即在瀏覽器視窗上有自己的矩形

  Inline box:並沒有自己的塊狀區域,但包含在一個塊狀區域內

  block一個挨著一個垂直格式化,inline則在水平方向上格式化。

  Inline盒模型放置在行內或是line box中,每行至少和最高的box一樣高,當box以baseline對齊時——即一個元素的底部和另一個box上除底部以外的某點對齊,行高可以比最高的box高。當容器寬度不夠時,行內元素將被放到多行中,這在一個p元素中經常發生。

  定位Position

  Relative

  相對定位——先按照一般的定位,然後按所要求的差值移動。

  Floats

  一個浮動的box移動到一行的最左邊或是最右邊,其餘的box圍繞在它周圍。下面這段html:

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

  將顯示為:

  Absolute和Fixed

  這種情況下的佈局完全不顧普通的文件流,元素不屬於文件流的一部分,大小取決於容器。Fixed時,容器為viewport(可視區域)。

圖17:fixed

  注意-fixed即使在文件流滾動時也不會移動。

  Layered representation

  這個由CSS屬性中的z-index指定,表示盒模型的第三個大小,即在z軸上的位置。Box分發到堆疊中(稱為堆疊上下文),每個堆疊中靠後的元素將被較早繪製,棧頂靠前的元素離使用者最近,當發生交疊時,將隱藏靠後的元素。堆疊根據z-index屬性排序,擁有z-index屬性的box形成了一個區域性堆疊,viewport有外部堆疊,例如:

<STYLE type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</STYLE>
<P>
<DIV
>
</DIV>
<DIV
>
</DIV>
</p>

  結果是:

  雖然綠色div排在紅色div後面,可能在正常流中也已經被繪製在後面,但z-index有更高優先順序,所以在根box的堆疊中更靠前。

  英文原文:How Browsers Work: Behind the Scenes of Modern Web Browsers

相關文章