前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

(本文很長,但值得看,完全搞懂瀏覽器)

1. 介紹

1.1 示例瀏覽器

主流瀏覽器:Internet Explorer, Firefox, Safari, Chrome and Opera

示例瀏覽器:Firefox、Chrome(開源)和 Safari(部分開源)

瀏覽器使用統計:http://gs.statcounter.com/

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

1.2 瀏覽器的主要功能(The browser's main functionality)

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

瀏覽器的用戶界面:

  • 輸入 URI 的地址欄
  • 回退、前進按鈕
  • 網址收藏
  • 刷新和停止按鈕
  • 首頁按鈕

HTML5 規範列出了瀏覽器的幾種通用元素:

  • 地址欄
  • 狀態欄
  • 工具欄

1.3 瀏覽器的高層架構(The browser's high level structure)

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

瀏覽器的組成部分:

  • 用戶界面

包含地址欄、回退和前進按鈕、網址收藏等。除了展示請求到的資源的主窗口,其他都是用戶界面。

  • 瀏覽器引擎

請求和操作渲染引擎的接口

  • 渲染引擎

負責展示請求的資源。比如說,請求到的資源是 HTML,渲染引擎就負責解析 HTML 和 CSS,然後將解析後的內容展示在屏幕上。

  • 網絡

網絡調用,如 HTTP 請求。它有跨平臺的獨立接口,且每個平臺都有自己的底層實現方式。

  • 用戶界面後端

繪製基本的窗體小部件,如組合框、視窗。它顯示一個非平臺特定的泛型界面。在底層,它使用操作系統的用戶界面方法。

  • JS 解析器

解析和執行 JS 代碼

  • 數據存儲

持久層。瀏覽器需要它來存儲硬盤上的各種數據,如 cookies。新的 HTML 規範(HTML5)定義了 網頁數據庫 —— 瀏覽器中的完整(輕量)的數據庫。

瀏覽器主要組成部分示意圖

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

值得注意的是,chrome 瀏覽器有多個渲染引擎實例——每個 tab 標籤頁一個每個 tab 標籤頁都是一個獨立的進程。(大家用Chrome就錯不了!!!)

2. 渲染引擎

2.1 渲染引擎(The rendering engine)

渲染引擎負責渲染,將請求到的內容呈現到屏幕上。

默認情況下,渲染引擎可顯示 HTML、XML文檔和圖片。也可通過插件(瀏覽器擴展程序)顯示其他類型。比如,可通過 PDF 閱讀器插件顯示 PDF 文件。

2.2 主流程(The main flow)

渲染引擎一開始從網絡層請求文檔內容。

接下來是以下的基本流程:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

渲染引擎先解析 HTML,將標籤轉換成樹的節點——叫作"內容樹"。然後解析樣式數據,包括外聯樣式和內聯樣式。樣式信息和 HTML 中的可視指令一起用於創建另一棵樹——渲染樹。

渲染樹包含具有可視屬性(如顏色和尺寸)的矩形。矩形按正確的順序顯示在屏幕上。

渲染樹構建完成後,就進入"佈局"流程。也就是給每個節點分配正確的座標——節點應該顯示在屏幕上的哪個位置。下一步就是繪製——將遍歷渲染樹,通過用戶界面(UI)後端層繪製每個節點。

重要的是要理解這是一個循序漸進的過程。為了更好的用戶體驗,渲染引擎將盡可能快地在屏幕上呈現內容。它不會等到所有的 HTML 都解析完成才開始構建和佈局渲染樹。當程序還在持續處理來自網絡層的內容時,它已經解析和呈現了部分內容。

2.3 主流程示例(Main flow examples)

Webkit 主流程:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

Mozilla Gecko 渲染引擎主流程:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4 解析和 DOM 樹構建(Parsing and DOM tree construction)

2.4.1 解析(Parsing-General)

由於解析是渲染引擎中非常重要的一個進程,所以我們會稍微深入講一下。首先介紹下解析。

解析文檔就是將文檔轉換成某種有意義的——對代碼來說可以理解和使用的——結構。解析的結果通常是代表文檔結構的節點樹。通常叫作解析樹或語法樹。

例如,解析 2 + 3 - 1,將返回如下樹:

數學表達式樹節點:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4.1.1 語法(Grammars)

解析基於文檔所遵循的語法規則——編寫它的語言或形式。每種可編譯的形式必須包含確定的語法——由詞彙和語法規則組成。這叫作 上下文無關語法。人類語言並不是這樣的,所以不能通過傳統的解析技術來解析。

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

解析可分為兩個子進程——詞法分析和語法分析。

詞法分析就是將輸入內容分解成詞法單元的過程。詞法單元就是語言詞彙——有效構建塊的集合。在人類語言中,詞法單元由那種語言的詞典中出現的單詞所組成。

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

解析器通常將工作分配給兩個不同的部件——詞法分析器(有時也叫分詞器),負責將輸入內容分解成有效的詞法單元;解析器,負責根據語法規則分析文檔結構來構建解析樹,

詞法分析器知道如何丟棄無關的字符,如空格和換行。

從源文檔到解析樹的過程

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

解析的過程是不斷重複的。解析器通常會向詞法分析器查詢一個新的詞法單元,然後將這個詞法單元與語法規則匹配,如果匹配到了,就將與這個詞法單元對應的節點添加到解析樹,然後繼續查詢下一個詞法單元。

如果匹配不到,解析器會將這個詞法單元儲存起來,繼續查詢其他詞法單元,直到找到一個能匹配所有儲存起來的詞法單元的語法規則為止。如果找不到匹配規則,解析器將拋出異常。這就是說文檔是無效的,且包含了語法錯誤。

2.4.1.3 翻譯(Translation)

很多時候,解析樹並不是最終的產物。解析通常用於翻譯——將輸入文檔轉換成另一種形式。比如說編譯。編譯器——將源代碼轉換成機器代碼——首先會將它轉換成解析樹,然後再將解析樹翻譯成機器代碼文檔。

編譯流程

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4.1.4 解析舉例(Parsing example)

數學表達式樹節點 圖中,已經通過數學表達式構建過一個解析樹。現在定義一個簡單的數學語言,然後看一下解析過程。

詞彙:這個語言可以包含整數、加號和減號。

語法:

  • 語法構建塊是表達式、術語、操作。
  • 這個語言可包含任何數量的表達式。
  • 一個表達式被定義為:一個術語後面跟著一個操作,操作後面跟著另一個術語。
  • 一個操作就是一個加號或一個減號
  • 一個術語是一個整數或一個表達式。

現在來分析下 "2 + 3 - 1"。

第一個匹配到規則的子串是 "2",根據規則 5,它是一個術語。 第二個匹配到的是 "2 + 3",匹配到第三條規則——一個術語後面跟著一個操作,操作後面跟著另一個術語。 下一個匹配只能是末尾了。"2 + 3 - 1" 是一個表達式,因為我們已經知道 ?2+3? 是一個術語,所以我們有一個術語後面跟著一個操作,後面跟著另一個術語。 "2 + +" 不能匹配任何規則,所以是無效的輸入。

2.4.1.5 詞彙和語法的正式定義(Formal definitions for vocabulary and syntax)

詞彙通常由正則表達式表示。

比如,上面的語言可以這樣定義:

整數(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

2.4.1.6 解析器種類(Types of parsers)

解析器有兩種基本類型——自上而下的解析器和自下而上的解析器。

直接的解釋就是:自上而下的解析器查找語法的高層結構,然後嘗試匹配其中的一個。自下而上的解析器從輸入開始,然後逐步地轉換成語法規則,從低級規則開始,直到匹配到高級規則為止。

現在來看下這兩種解析器如何解析我們的示例。

自上而下的解析器從高級規則開始,它將 "2 + 3" 定義成一個表達式,然後將 "2 + 3 - 1" 定義成一個表達式(表達式定義的過程逐步演化成匹配其他規則,但是起點是高級規則)。

自下而上的解析器會掃描輸入內容,直到找到匹配的規則,然後用這個規則去替換匹配的內容。這個過程將一直持續到輸入內容的末尾。部分匹配的表達式儲存在解析的堆內存中。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

自下而上的解析器也叫作移位解析器。因為輸入內容被移到右邊(想象一個指示器從輸入內容開始不斷移到末尾),然後逐漸地變成語法規則。

2.4.1.7 自動化解析器(Generating parsers automatically)

有些工具可以自動生成解析器,叫作解析器生成器。給解析器生成器提供語言的語法——詞彙和語法規則——它就能生成一個工作解析器。

創建解析器需要深入理解解析,而且手動創建一個優化的解析器也非常不易,所以解析器生成器就非常有用了。

Webkit 使用兩種眾所周知的解析器生成器——Flex,用於創建分詞器;Bison,用於創建解析器(有時候也可能叫做 Lex 和 Yacc)。

Flex 輸入是一個包含用正則表達式定義標記的文件。Bison 輸入是語言的語法規則和 BNF 形式。

2.4.2 HTML 解析器(HTML Parser)

HTML 解析器負責將 HTML 標記解析成解析樹。

2.4.2.1 HTML 語法定義(The HTML grammar definition)

HTML 的詞彙和語法規則由 W3C 組織的規範所定義。

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

在介紹解析時,提到了語法能通過 BNF 的形式定義。

然而,所有的傳統解析器都不適用於 HTML。HTML 不能輕易地由解析器所需的上下文無關語法定義。

定義 HTML 語法有正式的形式——DTD(Document Type Definition 文檔類型聲明)。但是它不是一種上下文無關語法。

初看之下這非常得奇怪。HTML 非常接近於 XML。有許多的 XML 解析器。還有一種 HTML 的變種——XHTML。所以差異在哪裡?

差異在於,HTML 非常的"寬容",它允許你省略隱式添加的標籤,有時還能省略開始或結束的標籤。基本上,它是一種"軟"語法,相反於 XML 的嚴格高要求的語法。

表面上看起來是很小的差異,然而卻是天差地別。

一方面,這也是為什麼 HTML 如此受歡迎——它會原諒你的錯誤,使網站作者感到更加舒適。另一方面,這使得寫範式語法非常得困難。

總得來說,HTML 不能輕易地被傳統解析器解析,因為它的語法不是上下文無關語法,也不能被 XML 解析器解析。

HTML 是用 DTD 形式定義的。這種形式用來定義 SGML (標準通用標記語言)語言。 這種形式包含了所有允許的元素的定義,它們的屬性和層級。之前已經看到,HTML 文檔類型聲明不會形成上下文無關語法。

DTD 有很多不同的類型。嚴格模式是唯一符合規範的,其他形式還包含了對瀏覽器過去使用的一些標籤的支持。目的是向後兼容舊的內容。

2.4.2.4 DOM(文檔對象模型)

輸出樹——解析樹,就是 DOM 元素和屬性節點的樹。DOM 是 Document Object Model 的縮寫。DOM 是 HTML 文檔的對象呈現,也是 HTML 元素與外界(如 JavaScript)的連接接口。

樹的根節點是 Document(文檔)。

DOM 和 標記有著幾乎一對一的關係。例如:

<html>
<body>
<p>Hello World.</p>
<div><img src="example.png"/></div>
</body>
</html>

上面內容會被轉換成:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

像 HTML 一樣,DOM 由 W3C 組織規定。

2.4.2.5 解析算法(The parsing algorithm)

如之前所說,HTML 不能被常規的自上而下或自下而上的解析器解析。

原因如下:

  • 語言的寬容特性
  • 瀏覽器具有傳統的容錯能力,以支持眾所周知的無效 HTML 案例。
  • 解析過程是可重入的(reentrant)。通常,在解析過程中,源是不會改變的,但是在 HTML 中,腳本元素包含 "document.write" 可以增加額外的標籤,所以解析過程實際改變了輸入的內容。

不能使用傳統的解析技術,瀏覽器創建了自定義的解析 HTML 的解析器。

解析算法由 HTML5 規範詳細描述。算法包含了兩個階段——分詞和樹的構建。

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

分詞器識別標記,把它給到樹構建器,然後繼續查找下一個字符,識別下一個標記,直到輸入的結束。

HTML 解析流程:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4.2.6 分詞算法(The tokenization algorithm)

這個算法的輸出是一個 HTML 標記。這個算法表現為一個狀態機。每個狀態消耗輸入了的一個或多個字符,然後根據這些字符更新下一個狀態。

這個決定受分詞狀態和樹的構建狀態所影響。這個算法太複雜,所以不能詳細講解,我們來看一個簡單的例子,大致地瞭解下。

分詞下面的 HTML:

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

初始的狀態是 "數據狀態"。當遇到 "<" 字符時,狀態就變成 "標籤打開狀態"。遇到 "a-z" 字符時,會創建 "開始標籤標記",狀態就變成 "標籤名稱狀態"。 直到遇到 ">" 這個字符時,這個狀態才會結束。每個字符都被添加到這個標記名稱下。在我們的例子中,創建的標記就是 "html"。

當遇到 ">" 時,當前的標記會被釋放,然後狀態變回 "數據狀態"。"<body>" 標籤也用相同的步驟來解析。現在,"html" 和 "body" 標記都已被釋放,狀態變回 "數據狀態"。 遇到 "Hello world." 的 "H" 時,會創建和釋放一個字符標記。這個過程持續到遇到 "</body>" 的 "<" 字符為止,此時會釋放 "Hello world." 的所有字符。

現在又回到了 "標籤打開狀態"。遇到 "/" 時會創建 "結束標籤標記",然後變成 "標籤名稱狀態"。同樣的,這個狀態持續到遇到 ">" 這個字符為止。 然後這個新的標籤標記會放釋放,再次回到 "數據狀態"。"</html>" 標籤也會用同樣的方式解析。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

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

當解析器創建好時,Document 對象也創建好了。在樹的構建階段,會改變包含 Document 根節點的 DOM 樹,還會添加元素到 DOM 樹。每個被分詞器釋放的節點都將被樹構建器加工。 對於每個標記,規範會定義與它相對應的 DOM 元素,並且為該元素創建這個 DOM 元素。除了將元素添加到 DOM 樹中外,還會將元素添加到一個開放元素的堆中。這個堆用於修正嵌套錯誤和未關閉的元素。 構建算法也是通過狀態機的形式表示的。這些狀態叫作"嵌入模式"。

來看一個樹構建的過程:

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

樹構建階段的輸入是一系列來自分詞階段的標記。第一個模式是 "初始模式"(initial mode)。接收 html 標記時,模式會轉變成 "html 前"(before html)模式,並會對這個模式下的標記進行回收。 此時,會創建 HTMLHtmlElement 元素,並添加到 根文檔對象(root Document object) 中。

之後又會轉變成 "head 前"(before head) 狀態。當遇到 "body" 標記時,HTMLHeadElement 元素會被隱式地創建,並被添加到 DOM 樹,雖然我們沒有 "head" 這個標記。

現在我們已經到了 "head 前" 模式,然後要進入到 "head 後"(after head) 模式。 現在對body 標記進行再加工,創建和插入 HTMLBodyElement 元素,此時又轉變成了 "body 內"(in body)模式。 現在接收 "Hello world." 這個字符串字符標記,第一個字母會使構建器創建和插入一個 文本節點(Text node),其餘的字符會被添加到這個節點中。

接收 body 的結束標記時,會轉變成 "body 後" (after body)模式。 之後會接收 html 的結束標記,此時會轉變成 "body 後後"(after after body) 模式。 接收到文件的結束標記時,解析也就結束了。

html 的樹構建:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4.2.8 解析結束的操作(Actions when the parsing is finished)

在這個階段,瀏覽器會標記文檔是可交互的,然後開始解析"延遲"模式(deferred mode)下的腳本——在文檔解析完成後執行。此時,文檔狀態變成 "完成",拋出一個 "load" 事件。

關於完整的 HTML5 規範的分詞和樹構建的算法,可參考:http://www.w3.org/TR/html5/syntax.html#html-parser

2.4.2.9 瀏覽器容錯(Browsers error tolerance)

在 HTML 頁面上,永遠不會出現 "無效語法"(Invalid Syntax)的錯誤。瀏覽器會修正它,然後繼續運行。

比如下面這段 HTML 代碼:

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

我肯定已經違反了大概100萬條規則("mytag" 不是一個標準的標籤,錯誤的 "div" 和 "p" 元素嵌套等),但是瀏覽器仍然正確的顯示,沒有任何抱怨。所以許多的解析器代碼都在修正 HTML 作者的錯誤。

不同瀏覽器的錯誤處理出奇得相當一致,雖然現有的 HTML 規範並未對此有規定。像書籤、前進/後退按鈕,就是瀏覽器多年發展以來的產物。

有許多周知的無效代碼結構,它們在許多網站不斷重複,瀏覽器會嘗試用與其他瀏覽器一致的方式修正它們。

HTML5 規範確實也有定義其中的一些要求。Webkit 在 HTML 解析器種類(HTML parser class)的開頭的註釋中,很好地總結了這些規範:

解析器將分詞的輸入解析成文檔,構建文檔樹。如果這個文檔結構良好,那麼解析文檔是很簡單的。然而,我們不得不處理許多結構不是很好的文檔,所以解析器不得不容錯。 我們得至少注意以下這些錯誤條件:

  • 1、明確禁止在外部標籤中添加的元素
  • 在這種情況下,我們應該閉合這些禁止添加元素的標籤,然後再添加。
  • 2、不允許直接添加元素
  • 這可能是這樣的,寫文檔的人忘記了一些中間的標籤(或者這些中間標籤是可選的)。
  • 這些標籤可能是:HTML BODY TBODY TR TD LI (還有嗎?)
  • 3、將塊級元素嵌套在行內元素中。閉合所有的塊級元素旁的行內元素。
  • 4、如果上面的都沒用,閉合所有的元素,除非允許添加元素和忽略標籤。

現在來看一些 Webkit 容錯的例子:

使用 </br> 代替 <br>

有些網站使用 </br> 代替 <br>,為了兼容 IE 和 火狐,Webkit 會像下面這樣對待它:

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);
}

很深的標籤層級

看下面的註釋,不言而喻:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

html 或 body 閉合標籤的位置放錯

再來看一下它的註釋:

支持破碎的 html 我們不會閉合 body 標籤,因為一些愚蠢的網頁會在文檔結束時閉合它。 我們依靠 end() 方法來閉合。

if (t->tagName == htmlTag || t->tagName == bodyTag )
return;

所有網頁開發者要注意了,除非你想你的代碼出現在 Webkit 容錯示例中,否則就寫結構良好的文檔。

2.4.3 CSS 解析(CSS parsing)

還記得什麼是解析的概念嗎?不像 HTML,CSS 是上下文無關語法,可以通過之前介紹的解析器解析。 事實上,CSS 的詞法語法和句法語法由 CSS 規範規定,參考:https://www.w3.org/TR/CSS2/grammar.html

來看一些例子:

詞法語法(詞彙)是通過正則來定義每個標記的。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

"ident" 是 "identity" 的縮寫,比如一個類名(class name)。 "name" 是一個元素 id(由 # 來引用)。

句法語法由 BNF 來描述。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

解釋:ruleset 的結構如下:

div.error, a.error {
color:red;
font-weight:bold;
}

div.error 和 a.error 是選擇器。大括號內的內容會應用到這個 ruleset 上。這個結構會用下面的方式定義:

ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;

這就是說,一個 ruleset 就是一個選擇器或者多個由逗號和空格分隔的選擇器(S 代表空格)。一個 ruleset 包含一個大括號,以及括號內的一個或多個由分號分隔的聲明。 "聲明"(declaration)和 "選擇器"(selector)由下面的 BNF 定義。

2.4.3.1 Webkit CSS 解析器(Webkit CSS parser)

Webkit 使用 Flex 和 Bison 解析器生成器從 CSS 語法文件中自動生成解析器。

回憶一下上面對解析器的介紹一節,Bison 創建一個自下而上的移位遞減解析器。

Firefox 使用手寫的自上而下的解析器。

在兩種情況下,CSS 文件都會被解析成 樣式表(StyleSheet)對象,每個對象包含 CSS 規則。每個 CSS 規則包含選擇器、聲明對象和其他與 CSS 語法相對應的對象。

解析 CSS:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.4.4 解析腳本(Parsing scripts)

本章主要會通過 JavaScript 來講解。

2.4.5 處理腳本和樣式表的順序(The order of processing scripts and style sheets)

2.4.5.1 腳本(Scripts)

網絡模型是同步的。作者們都希望解析器遇到 <script> 標籤時能立即解析和執行。文檔解析會處於被阻塞狀態,直到腳本執行完畢。

如果是外部腳本,那麼先得從網上獲取資源——這也是同步的,文檔解析會處於被阻塞狀態,直到資源獲取完畢。

多年來,一直都是這種模型,也是 HTML4 和 HTML5 規範規定的模型。作者可以將腳本標記為"延遲"(defer),這樣不會阻塞文檔的解析,解析完成後,就會執行。 HTML5 增加了一個選項,可以把腳本標記成異步的,這樣就可以在另一個線程中進行解析和執行。

2.4.5.2 預解析(Speculative parsing)

Webkit 和 Firefox 都做了這種優化。在執行腳本的過程中,另一個線程解析文檔的剩餘部分,找出還需要從網上加載哪些資源,然後加載它們。 這樣,資源可以平行加載,總體速度也會更快。

注意:預解析器不會改變 DOM 樹,DOM 樹還是會留給主解析器,它只會解析外部資源的引用,如外部腳本、樣式和圖片等。

2.4.5.3 樣式表(Style sheets)

樣式表有著不同的模型。概念上,看起來樣式表不會變成 DOM 樹,所以沒必要等待和阻塞文檔解析。 但是,在文檔解析階段,腳本會向樣式信息問一個問題。如果樣式還沒加載和解析,腳本會得到錯誤的答案,顯然,這會導致很多的問題。 這似乎是一個邊界情況,但是卻相當普遍。

當有樣式在加載和解析時,Firefox 會阻塞所有的腳本。Webkit 僅會阻塞這些試圖獲取特定的樣式屬性——這些屬性可能會受未加載的樣式影響——的腳本。

2.5 渲染樹構建(Render tree construction)

構建 DOM 樹的同時,瀏覽器還在構建另一個樹——渲染樹。渲染樹由可視元素組成,這些元素按將要展示的順序排列。它是文檔的視覺呈現。 渲染樹的目的是保證內容有序繪製。

Firefox 把渲染樹中的元素叫作 "幀"(frames)。

Webkit 把這些元素叫作 "渲染器"(renderer)或 "渲染對象"(render object)。

一個渲染器知道如何佈局和繪製自身及其子類。

Webkit 的渲染器對象的基本類—— RenderObject 類的定義如下:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

每個渲染器代表了一個矩形區域,與 CSS2 規範定義的節點的 CSS 盒子(盒模型)相對應。它包含了幾何信息,如寬、高和位置。

盒子的類型由與節點相對應的 "display" 樣式屬性決定。

下面的是 Webkit 的代碼,通過 "display" 屬性決定應該為 DOM 節點生成何種渲染器。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

還考慮了元素類型,如表單控件和表格有特殊的幀。

在 Webkit 中,如果一個元素想創建一個特殊的渲染器,那麼它會覆寫 createRenderer 這個方法。這些渲染器指向包含了非幾何信息的樣式對象。

2.5.1 渲染樹與 DOM 樹的關係(The render tree relation to the DOM tree)

渲染器與 DOM 元素相對應,但並不是一對一的關係。非可視元素不會被插入到渲染樹,比如 head 元素。 還有 display 屬性設置為 none 的元素也不會出現在渲染樹中(visibility 設置為 hidden 的元素會出現在渲染樹中)。

有些 DOM 元素會對應多個可視對象。有些元素結構比較複雜,所以不能用單個矩形來表示。比如,select 元素有3個渲染器,一個渲染區域,一個渲染下拉列表框,一個渲染按鈕。 當文本在一行內顯示不下,被拆分成多行時,新行中的文本會被添加到新的渲染器中。

另一個多個渲染器的例子是拆分的 HTML。CSS 規範規定一個行內元素只能包含塊級元素或只能包含行內元素。 如果行內元素既包含了行內元素又包含了塊級元素,會創建一個匿名塊級渲染器包裹這些行內元素。

有些渲染對象與 DOM 節點一對一對應,但是在樹中的位置卻不同。浮動和絕對定位的元素脫離了流,放置在樹的不同位置,然後映射到實際的幀。它們存在於佔位符幀中。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

渲染樹和與之對應的 DOM 樹,Viewport 是初始的包含塊。在 Webkit中,Viewport 是 RenderView 對象。

2.5.2 構建樹的過程(The flow of constructing the tree)

在 Firefox 中,構建過程表現為為 DOM 的更新註冊一個監聽器(listener),然後將幀的創建委派給 "幀構建器",構建器會分解樣式(見下面的樣式計算),創建幀。

在 Webkit 中,分解樣式和創建渲染器的過程叫作 "附著"(attachment)。每個 DOM 節點都有一個 attach 方法。"附著" 是同步的,節點插入到 DOM 樹中會調用新節點的 attach 方法。

渲染樹根節點構建時會處理 html 和 body 標籤。根渲染對象與 CSS 規範中的包含塊——最頂端的包含所有其他塊的塊——相對應。 包含塊的大小就是視口大小——瀏覽器窗口展示的區域大小。Firefox 把它叫做 "視口幀"(ViewPortFrame),Webkit 把它叫做 "RenderView"(渲染視口)。 這就是文檔指向的渲染對象。樹的其他部分被構建為 DOM 節點插入。

CSS2 關於這個話題的的資料: http://www.w3.org/TR/CSS21/intro.html#processing-model

2.5.3 樣式計算(Style Computation)

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

樣式包含了不同來源的樣式表,如內聯樣式元素、HTML 中的可視屬性(比如 bgcolor 屬性)。後者被轉換,匹配 CSS 的樣式屬性。

樣式表來源於瀏覽器的默認樣式表、網頁作者提供的樣式表、用戶樣式表——這些樣式表由瀏覽器用戶提供(瀏覽器允許用戶定製喜歡的風格,比如,在 Firefox 中,可以在 Firefox fold 中放置一份樣式表即可)。

樣式計算帶來了一些難題

  • 1、樣式數據的結構龐大,包含了許多的樣式屬性,可能會引起內存問題。
  • 2、如果沒有優化,那麼為每個元素查找匹配規則會導致性能問題。為每個元素查找匹配遍歷整個規則表是一項繁重的任務。 選擇器可以有複雜的結構,這會導致匹配過程會從看上去有希望的路徑開始匹配,而實際上卻是無效的,然後再去嘗試新的匹配路徑。
  • 3、應用規則涉及非常複雜的級聯規則,這些規則定義了規則的層次結構。

比如下面的複合選擇器:

div div div div{
// ...
}

上面的代碼意思就是匹配一個 <div>,它是三個 <div> 的後代。 假如你想驗證這個匹配規則是否適用於某個 <div> 元素,你從樹上選某個路徑開始驗證,你需要遍歷節點樹去找到三個 <div>,結果卻只找到兩個,匹配規則並不生效。 你就得從節點樹的另一個路徑開始查找。

  • 應用規則涉及非常複雜的級聯規則,這些規則定義了規則的層次結構。

我們來看看瀏覽器怎麼解決這些問題:

2.5.3.1 共享樣式數據(Sharing style data)

Webkit 節點引用樣式對象(渲染樣式 RenderStyle)。這些對象在某些情況下,可以被節點共享。這些節點是兄弟節點以及:

  • 1、這些元素必須在相同的鼠標狀態下(比如,不能一個是 :hover 狀態,其他的不是)
  • 2、元素不應該有 ID
  • 3、標籤名稱應該能匹配
  • 4、class 屬性應該能匹配
  • 5、映射屬性集必須完全相同
  • 6、link 狀態必須匹配
  • 7、focus 狀態必須匹配
  • 8、元素不能受到屬性選擇器的影響,影響被定義為可匹配到使用了元素中的任何屬性的屬性選擇器。
  • 9、元素不能存在行內樣式屬性
  • 10、不能使用兄弟選擇器。WebCore 遇到兄弟選擇器時會拋出一個全局開關,為整個文檔關閉樣式共享。這些選擇器包括:+ 選擇器,:first-child 和 :last-child 等。

2.5.3.2 Firefox 規則樹(Firefox rule tree)

為了更簡單的樣式計算,Firefox 提供了兩種樹——規則樹和樣式上下文樹。Webkit 也有樣式對象,但是他們不是儲存在類似於樣式上下文樹的樹中,它只有 DOM 節點指向相應的樣式。

Firefox 樣式上下文樹:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

樣式上下文包含了端值(end values)。值的計算是通過按正確的順序應用匹配規則,以及執行將它們從邏輯值轉換成具體值的操作來完成的。 比如,如果邏輯值是屏幕的百分百,它會被計算並轉換成絕對的單位。規則樹的想法相當得聰明。它允許在不同節點間共享這些值,避免重複計算。同時也節省了空間。

所有匹配到的規則都會儲存在樹中,在一條路徑中,越下面的節點擁有越高的優先級。樣式上下文樹包含了找到的規則匹配的所有路徑。規則的儲存是惰性的。 樣式上下文樹不會一開始就為每個節點進行計算,而是當節點需要被計算時,將計算路徑添加到樹中。

我們的想法是將樹路徑看成是詞典中的單詞。假設我們已經計算好了規則樹:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

假設,我們需要在內容樹中為其他元素匹配規則,找到的規則(按正確的順序)是:B - E - I。這個規則已經存在於樹中,因為已經計算過 A - B - E - I - L 的路徑。現在我們要做的工作就少了。

2.5.3.2.1 分結構(Division into structs)

樣式上下文被分成不同的結構。每個結構都包含了某種分類(如border或color)的樣式信息。 結構中的所有屬性(properties)或者是繼承的或者是非繼承的。繼承屬性除非在元素上有定義屬性,一般會繼承父級。 非繼承屬性(也叫重置屬性)如果未定義,一般會使用默認的。

樣式上下文樹會幫忙將樹中的結構緩存起來(包括計算的端值)。如果下層節點沒有為結構提供定義,就用緩存中的上層節點的的結構。

2.5.3.2.2 使用規則樹計算樣式上下文(Computing the style contexts using the rule tree)

為某個元素計算樣式上下文時,首先會去計算規則樹中的路徑,或者使用已經存在的路徑。然後在路徑中應用規則填充新樣式上下文中的結構。 我們從路徑的底層節點——優先級最高的節點(通常是最具體的選擇器)開始,然後往上遍歷上下文樹,直到將結構填充完成。 如果在規則樹中沒有對該結構的規定,那麼我們就可以大幅優化——我們在樹中往上找,找到某個節點能完整得規定並指向這個結構——這是最好的優化——因為這整個結構是完全共享的。 這樣節省了端值的計算和內存。如果我們只找到部分定義,那麼繼續在樹中往上找,直到將整個結構填充完整。

如果我們沒有找到對該結構的任何定義,假如整個結構是一個"繼承"類型,就將該結構指向上下文樹的父級結構,在這種情況下,我們也成功共享了結構。 如果是重置結構,那麼將使用默認值。

如果最具體的節點有添加值,那麼我們需要進行額外的計算,將這些值轉成實際的值,然後將結果儲存在樹節點中,供子節點使用。

假如一個元素有子節點或兄弟節點,它們指向相同的樹節點,那麼整個樣式上下文(entire style context)可以在它們之間共享。

來看一個例子,

假設有這麼一段 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>

以及規則如下:

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 struct)和邊緣空白結構(margin struct)。顏色只包含一個成員,而邊緣包含了四個邊。 規則樹看起來如下(節點以節點名——即序號標記)和語法樹如下(節點名,指向規則節點):

規則樹(右側)和上下文樹(左側):

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

上下文樹

假設我們現在解析這段 HTML 代碼,遇到第二個 <div> 標籤,我們需要為這個節點創建一個樣式上下文,然後填充它的樣式結構。 匹配規則時,發現這個 <div> 標籤匹配到的規則是 1、2、6,這就是說,在樹中已經存在當前元素可以使用的路徑,現在僅需要將規則 6(規則樹中的 F) 添加到另一個節點中。 然後創建一個樣式上下文,將它存到上下文樹中。新的樣式上下文將指向規則樹中的節點 F。可參考下圖。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

現在填充樣式結構,從 margin 結構開始,因為最後一個規則節點(F)沒有加到 margin 結構中, 我們可以在樹中繼續往上找,直到找到之前的節點插入時計算並儲存起來的結構,並使用它。 最後在節點 B 中找到——規定了 margin 規則的最上面的節點。

因為對顏色結構有定義,所以不能使用緩存的結構。因為顏色有一個屬性,所以我們不需要在樹中填充其他屬性。我們將計算端值(end value)——將字符串轉成 RGB 等——然後在這個節點中儲存計算好的結構。

解析第二個 <span> 元素的工作更加簡單。我們匹配規則,發現它指向規則 G,和之前的 <span> 一樣。子節點指向了同一個節點,所以可以共享整個樣式上下文,指向前一個 span 元素的上下文。

對於包含繼承自父級規則的結構,是儲存在上下文樹中的(color 屬性實際上繼承的,但是 Firefox 把它當成重置屬性,並緩存在規則樹中)。

比如,我們給 paragraph 增加了 fonts 規則:

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

上下文樹中 p 元素的子級(child) —— div 元素就可以共享父級的相同 font 結構。前提是 div 元素沒有規定 font 規則。

在 Webkit 中沒有規則樹,所以匹配聲明會被遍歷4次。

  • 首先,重要的高優先級的規則。(這些規則優先應用,因為其他屬性會依賴於它們,如 display)
  • 其次,高優先級重要的規則。
  • 然後,普通優先級不重要的規則。
  • 最後,普通優先級重要的規則。

這就是說,多次出現的屬性會根據正確的層級順序進行解析,明顯後者(上下文規則樹)勝出。

總結一下,共享樣式對象(全部或部分結構)解決了問題 1 和 3。Firefox 的規則樹對於按正確順序應用屬性也很有幫助。

2.5.3.3 操作規則以便輕鬆匹配(Manipulating the rules for an easy match)

樣式規則的來源:

  • CSS 規則,外部或內部樣式表
p { color: blue; } 
  • 行內樣式屬性
<p style="color: blue;"></p>
  • HTML 視覺屬性(映射到對應的樣式規則)
<p bgcolor="blue"></p>

後面兩個很容易匹配到,因為它擁有樣式屬性和 HTML 屬性,可以將元素當做 key 來進行映射。

正如之前提到過的問題 2,css 規則匹配更加棘手。 為了解決這個問題,規則被改成更加容易訪問。

解析完樣式表後,根據選擇器,將訪問規則添加到其中的某個哈希映射中。 有ID映射,類名(class name) 映射,標籤名(tag name)映射,和一個普通映射包含了其他不屬於前面幾種分類的選擇器。 如果選擇器是 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" 的 key,在該 key 下可以找到 "p.error" 的規則。 div 元素可以在 ID映射表(key 為 id)和標籤名映射表中找到相應的規則。

所以現在唯一的工作就是,找出按 key 提取出的規則中哪些規則是真正匹配的。

比如,div 的規則是這樣的:

table div {
margin: 5px;
}

現在仍是從 標籤映射表 中提取,但是 key 是最右邊的選擇器,但是它不會匹配上面的 div,因為它沒有 table 祖先。

對於上面的問題,Webkit 和 Firefox 都做了相應的操作(manipulation)。

2.5.3.4 按正確的層級順序應用規則(Applying the rules in the correct cascade order)

樣式對象的屬性與每個視覺屬性相對應。如果某個屬性沒有被任何匹配的規則所定義,那麼就可以從父級元素的樣式對象上繼承某些屬性。其他的屬性使用默認值。

當有多個定義時,問題就出現了——層級順序就是為了解決這個問題的。

2.5.3.4.1 樣式表層疊順序(Style sheet cascade order)

一個樣式屬性的聲明可以在多個樣式表中出現,也可在同個樣式表中出現多次。這就意味著規則的應用順序非常重要。這叫作 "層級" 順序。 根據 CSS2 的規範,層級順序如下(從低到高):

  • 瀏覽器聲明(Browser declarations)
  • 用戶的標準聲明(User normal declarations)
  • 作者的標準聲明(Author normal declarations)
  • 作者的重要聲明(Author important declarations)
  • 用戶的重要聲明(User important declarations)

瀏覽器聲明是最不重要的,用戶聲明僅在設為重要(important)時才會覆蓋作者的聲明。 順序相同的聲明會根據指定的順序來分類,然後按指定的順序來排序。 HTML 視覺屬性會被翻譯並匹配 CSS 的聲明。這些被認為是低優先級的用戶規則。

2.5.3.4.2 指定順序(Specifity)

選擇器順序指定由 CSS2 規範規定,具體如下:

  • 如果聲明是來自 "style" 屬性,而不是來自選擇器規則,就加1,否則就加0(=a)
  • 累加選擇器中 ID 屬性的數量(=b)
  • 累加選擇器中的其他屬性和偽類的數量(=c)
  • 累加選擇器中的元素名和偽元素的數量(=d)

連接數字 a-b-c-d (在一個大型的數字系統中)算出指定順序。

需要用到的基數由以上四個類型中數字最大的來決定。

比如說,a = 14,那麼可以使用十六進制。當 a = 17時,需要一個17位數的基數。 後者發生的情況是有一個類似於這樣的選擇器:html body div div p ... (選擇器中有17個標籤,雖然可能性不是很大)。

一些示例:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.5.3.4.3 規則排序(Sorting the rules)

樣式規則匹配完成後,會根據層級規則對其進行排序。 對於數據量小的列表,Webkit 使用冒泡排序(bubble sort),而對於數據量大的列表,使用混合排序(merge sort)。

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

2.5.4 漸進的過程(Gradual process)

Webkit 使用一個標誌來標記頂層樣式表是否加載完成(包括 @imports)。 當使用樣式時,發現樣式沒有完全加載完成,將會使用佔位符,並且在文檔中進行標記,當樣式加載完成時,會重新進行計算。

2.6 佈局(Layout)

渲染器創建完成並被添加到渲染樹時,它沒有位置(position)和 大小(size)。

計算這些值的過程叫作 佈局(layout)和 迴流(reflow)。

HTML 使用基於流的佈局模型,這意味著大多數時候能一次性計算出幾何結構。 流後面的元素不會影響流前面的元素的幾何結構。所以佈局可以按從左到右、從上到下的順序進行。 但也有例外——比如 tables,就需要多次計算(3.5)

座標系統和根框架相關,使用上側和左側座標。

佈局是一個遞歸的過程。從根渲染器開始,與 HTML 文檔的元素相對應。佈局會在其中一些或所有框架層級中持續遞歸,為每個渲染器計算幾何信息。

根渲染器的位置是 0,0,它的大小是視口大小——瀏覽器窗口的可視部分。

所有的渲染器都有一個 layout(佈局) 和 reflow(迴流) 方法,每個渲染器都會調用那些需要佈局的子渲染器的 layout 方法。

2.6.1 髒值系統(Dirty bit system)

為了避免為每個小的變動都進行一次完整的佈局,瀏覽器使用了髒值系統。

一個渲染器改變或添加了之後會標記自己及其子代為“dirty”——需要佈局。

有兩種標誌——“dirty”和“children are dirty”。後者意味著雖然渲染器本身沒問題,但是它至少有一個需要佈局的子代。

2.6.2 全局和遞增佈局(Global and incremental layout)

佈局能在整個渲染樹上觸發——這就是全局佈局(global layout)。以下幾種情形會引起全局佈局:

  • 1、會影響到所有渲染器的全局樣式改變,比如字體大小的改變。
  • 2、屏幕大小調整。

佈局可以是遞增的,只有髒的渲染器會被佈局(這可能會導致某些不好的影響,會引起額外的佈局)。

渲染器髒了就會(異步地)觸發遞增佈局(incremental layout)。比如,當網絡請求到新的內容,添加到 DOM 樹後,就會有新的渲染器被添加到渲染樹。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

遞增佈局——只有髒的渲染器及其子代會被佈局

2.6.3 異步和同步佈局(Asynchronous and Synchronous layout)

遞增佈局是異步完成的。Firefox 會為遞增佈局進行“迴流請求”(reflow commands)排隊,然後通過調度程序觸發這些請求的批量執行。Webkit 也有一個執行遞增佈局的定時器——遍歷渲染樹,佈局髒渲染器。

請求樣式信息,如 “offsightHeight” 的腳本會觸發同步遞增佈局。 全局佈局通常都是同步觸發的。

有時候初始佈局後的回調也會觸發佈局,因為一些屬性(比如滾動位置)發生改變。

2.6.4 優化(Optimizations)

如果佈局的觸發是由於大小調整(resize)或者渲染器的位置(position)改變——而不是大小改變,渲染器的大小會從緩存中獲取,不會重新計算。

在某些情況下,當只有子渲染樹改變時,佈局不會從根節點開始。發生這種情況的情形有:局部改變,不會影響周圍——比如文本插入到文本域中。否則的話,每次按鍵都會觸發從根節點開始的佈局。

2.6.5 佈局過程(The layout process)

佈局通常有下面幾種模式:

  1. 父渲染器決定自己的寬度
  2. 父渲染器遍歷子渲染器,然後
  3. 放置子渲染器(設置它的x和y)
  4. 如有需要,調用子渲染器的layout(佈局)方法——它們是髒的或者我們在全局佈局中,或者其他某些原因——這會計算子渲染器的高度。
  5. 父渲染器使用子渲染器的高度、外邊距、內邊距的累加高度來設置自己的高度——父渲染器的父渲染器也會使用這個高度。
  6. 設置髒值為false。

Firefox 使用狀態(state)對象(nsHTMLReflowState)作為 layout 方法的參數(術語叫“reflow”)。其中包含了父渲染器的寬度。

Firefox layout 方法的輸出是一個度量(metrics)對象(nsHTMLReflowMetrics)。它包含渲染器計算的高度。

2.6.6 寬度計算(Width calculation)

渲染器的寬度是使用容器塊的寬度——渲染器的樣式 “width” 屬性和 margins 和 borders 來計算的。

比如下面 div 的寬度:

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

Webkit 的計算方式如下(RenderBox類,calcWidth方法):

  • 容器的寬度是容器可用寬度(availableWidth)和0的最大值。在上面這個例子中,可用寬度就是內容寬度(contentWidth),計算方式如下:
clientWidth() - paddingLeft() - paddingRight()

clientWidth 和 clientHeight 指的是一個對象的內部寬高,除去邊(border)和滾動條(scrollbar)。

  • 元素的寬就是樣式屬性“width”的值,這個值是一個相對值,通過計算父容器的寬度的百分比得到。
  • 然後添加橫向邊和內邊距(horizontal borders and paddings)。

目前為止,這是“首選寬度”(preferred width)的計算,接下來計算最小寬度和最大寬度(minimum and maximum width)。如果首選寬度的值比最大寬度的值大,將使用最大寬度的值。如果首選寬度的值小於最小寬度的值(最小的不可分割的單位),將使用最小寬度的值。

這些值會緩存起來,以防在寬度沒有改變的情況下需要重新佈局。

2.6.7 折行(Line Breaking)

在一個渲染器在佈局過程中發現需要折行,它會停下來,通知父渲染器它需要折行。父渲染器就會創建額外的渲染器,然後調用這些渲染器的layout方法。

2.7 繪製(Painting)

在繪製階段,會遍歷渲染器樹,調用渲染器的 paint 方法,在屏幕上排列內容。繪製使用 UI 基礎組件,可以查看 UI 章節瞭解更多。

2.7.1 全局和遞增(Global and Incremental)

和佈局(layout)一樣,繪製也可以是全局的——整個樹繪製,或遞增繪製。在遞增繪製時,有些渲染器會以某種方式改變,但不會影響整個樹。改變了的渲染器會使自己在屏幕上的矩形位置無效。這會使得 OS 認為它是髒區域,然後生成一個 paint 事件。OS 巧妙地處理,然後將這些區域合併成一個。在 Chrome 中更加複雜,因為渲染器和主流程不在同一個進程中。Chrome 中某些程度上模仿了 OS。繪製過程(presentation)監聽這些事件,並將信息傳遞給渲染根節點。它會遍歷渲染樹,直到找到相關的渲染器。它會重新繪製自己(通常也會重新繪製它的子級)。

2.7.2 繪製順序(The painting order)

CSS2 規定了繪製程序的順序——http://www.w3.org/TR/CSS21/zindex.html。這實際上就是元素在層疊上下文(stacking context)中如何層疊的順序。

這個順序影響了繪製,因為層疊是從後到前的順序繪製的。一個塊級渲染器的層疊順序如下:

  1. background color(背景顏色)
  2. background image(背景圖片)
  3. border(邊)
  4. children(子級)
  5. outline(輪廓)

2.7.3 Firefox 排列列表(Firefox display list)

Firefox 遍歷渲染樹,然後為繪製的矩形創建一個排列列表。它包含了與矩形相對應的按正確繪製順序(渲染器的 backgrounds,borders 等)排序的渲染器。

這樣,渲染樹只需要遍歷一次,而不用遍歷很多次——繪製所有的背景、所有的圖片、所有的邊等。

Firefox 對繪製過程做了優化:不添加看不見的元素,如完全位於不透明元素下面的元素。

2.7.4 Webit 矩形存儲(Webkit rectangle storage)

在重新繪製前,webkit 將老的矩形存儲為位圖,然後只繪製這些處於新老矩形之間的區域。

2.8 動態改變(Dynamic changes)

瀏覽器嘗試對變化採取最小可能的行為。所以元素顏色的變化只會導致元素的重繪。元素位置的變化會導致元素及其子代或者同級元素的佈局和重繪。增加 DOM 節點會導致節點的佈局和重繪。重要的改變,如 html 元素的 font size,會導致緩存無效,然後觸發整個樹的重新佈局和重繪。

2.9 渲染引擎線程(The rendering engine's threads)

渲染引擎是單線程。幾乎所有的操作,除了網絡操作,都是單線程。在 Firefox 和 Safari 中,這是瀏覽器的主線程。 在 Chrome 中,渲染引擎是 tab 進程的主線程。

網絡操作可以以多個並行線程的方式執行。並行連接的個數有限制(通常是2-6個,Firefox3 使用 6個)。

2.9.1 事件循環(Event loop)

瀏覽器的主線程是一個事件循環。它是使進程保持活躍的無限循環。它等待事件(比如 layout 和 repaint事件),然後處理它們。下面是 Firefox 主要事件循環的代碼:

while (!mExiting)
NS_ProcessNextEvent(thread);

2.10 CSS2 視覺模型(CSS2 visual model)

2.10.1 畫布(The canvas)

根據 CSS2 規範,術語 canvas 就是 “渲染格式化的結構的空間”——瀏覽器繪製內容的地方。

根據 http://www.w3.org/TR/CSS2/zindex.html,被包含在其他 canvas 中的 canvas 是透明的,否則,就給定一個瀏覽器定義好的顏色。

2.10.2 CSS 盒模型(CSS Box model)

CSS 盒模型描述了在文檔樹中為元素生成的矩形盒子,然後通過視覺格式模型對其佈局。每個盒子有一個內容區域(比如 text,image等),以及可選的周邊區域,如內邊距、邊、外邊距。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

  • CSS2 盒模型*

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

每個元素都有一個 display 屬性,決定了如何生成它們的盒子的類型。

比如:

block -- 生成一個塊級盒子
inline -- 生成一個或多個行內盒子
none -- 不會生成盒子

默認是 inline,但是瀏覽器樣式表可以設置其他默認值。比如,div 元素的默認值是 block.

關於默認樣式表的例子,可以查看http://www.w3.org/TR/CSS2/sample.html。

2.10.3 定位方案(Positioning scheme)

有三種定位方案:

  1. Normal —— 對象根據在文檔中的位置排列——對象在渲染樹中的位置和在 DOM 樹中的位置是一樣的,並且根據盒子的類型和大小來佈局。
  2. Float —— 對象顯示按正常流佈局,然後儘可能地往左或右移動。
  3. Absolute —— 對象在渲染樹中的位置和 DOM 樹中的位置完全不一樣。

定位方案由 position 屬性和 floate 屬性決定:

  • static 和 normal 是正常流定位
  • absolute 和 fixed 是絕對定位

在 static 定位中,不定義任何位置,並使用默認定位。在其他方案下,作者定義位置——top、bottom、left、right。

盒子佈局的方法由下面這些條件決定:

  • 盒子類型(Box type)
  • 盒子大小(Box dimensions)
  • 位置方案(Position scheme)
  • 其他信息——比如圖片大小和屏幕大小

2.10.4 盒子類型(Box types)

塊級盒子:形成一個塊——在瀏覽器窗口中有自己的矩形區域。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

塊級盒子

行內盒子:沒有自己的塊,包含在塊中。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

行內盒子

塊級盒子按垂直方向排列,行內盒子按水平方向排列。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

塊級和行內盒子排版

行內盒子被放置在行內或行盒子中,當盒子以 baseline 的方式——元素的底部和另一個元素的某個點對其——來對齊時,這些行的高度至少和最高的盒子一樣高,但是不能比它高。如果行的寬度不夠寬,行會被放到很多行中。在 paragraph 中經常出現。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.10.5 定位(Positioning)

2.10.5.1 相對定位(Relative)

相對定位——像往常一樣定位然後按要求的delta移動。

相對定位

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

2.10.5.2 浮動(Floats)

一個浮動盒子會被移動到行的左邊或右邊。有趣的是其他盒子圍繞它流動。比如下面的 HTML:

<p>
<img style="float:right" src="images/image.gif" width="100" height="100">Lorem ipsum dolor sit amet, consectetuer...
</p>
前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

浮動

2.10.5.3 (絕對和固定定位)Absolute and fixed

無論正常流如何,都會精確定義佈局。元素不會出現在正常流中。元素的大小是相對於容器的。在 fixed 中,容器是視口。

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

固定定位

注意:即使文檔滾動了,固定盒子也不會移動!

2.10.6 分層呈現(Layered representation)

它由 z-index 屬性決定。代表了元素的第三個尺寸,它的 z 軸位置。

盒子被分成層級(叫做層級上下文)。在層級中,後面的元素會先繪製,前面的元素會繪製在上面,更接近用戶。

層級根據 z-index 屬性來排序。具有 z-index 屬性的盒子形成本身的層級。視口有外部層級。

比如:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

結果如下:

前端面試基礎:每天和瀏覽器打交道,你知道瀏覽器的工作原理嗎?

固定位置

雖然紅色 div 位於綠色之前,並且之前已經在常規流中繪製過,但是z-index屬性更高,因此它在根框所持有的堆棧中更向前。

感謝閱讀,歡迎關注,更多幹貨

相關推薦

推薦中...