瀏覽器是如何工做的?(How browser work?)

瀏覽器是如何工做的(How browser work)

1. 介紹

瀏覽器可謂是使用最普遍的軟件. 這篇文章我將要解釋瀏覽器在底層是如何工做的. 咱們將會了解當你在瀏覽器地址欄裏輸入'google.com'直到頁面呈現出來這一過程都發生了什麼。html5

1.1 本文涉及到的瀏覽器

目前市面上主要有5款瀏覽器: Internet Explorer, Firefox, Safari, Chrome 以及 Opera。
我將使用開源的瀏覽器中進行舉例,包含Firefox, Chrome 以及 部分開源的Fafari。
根據W3C browser statistics, 當前時間是2009年10月,使用Firefox, Safari 以及 Chrome 的比例佔據將近60%。
因此目前開源瀏覽器佔據的瀏覽器市場很大的份額。linux

1.2 瀏覽器的主要功能

瀏覽器的主要功能是把你從服務器請求到的網絡資源呈如今瀏覽器窗口上。資源一般包含了HTML,PDF, 圖片等等。資源一般是由用戶指定的URI(Unifor resource Identifier 統一資源定位符)來定位的。稍後的章節會介紹。git

瀏覽器解釋和呈現HTML文件的方式是經過HTML和CSS規範來實現的。 這些規範是由W3C組織進行維護的,該組織是互聯網的標準制定者。長久以來各個瀏覽器廠商只實現了一部分規範,而且開發本身的擴展程序。這致使了在不一樣的瀏覽器當中很嚴重的兼容性問題。到目前爲止,大部分的瀏覽器都大多實現了規範。
不一樣的瀏覽器UI有不少相同的部分:github

  • 輸入URI的地址欄
  • 前進和後退按鈕
  • 書籤操做
  • 操做當前加載文檔的刷新和中止按鈕
  • 返回主頁的主頁按鈕

可是比較奇怪的是, 瀏覽器的UI並無一個通用的規範,它只是不一樣的瀏覽器廠商從長期的使用習慣中積累的經驗。HTML5規範並無規定瀏覽器的UI必須包含哪些元素,只是列出了一些通用的元素。地址欄、狀態欄、工具欄以及各個瀏覽器指定的特定,例如Firefox的下載管理。更多參見用戶界面章節。web

1.3 瀏覽器的主要結構

瀏覽器的主要組成部分:算法

  1. 用戶界面(The user interface) - 包含地址欄、前進/後退按鈕、書籤等等。除了主要的窗口以外你所看到的就是請求的頁面。
  2. 瀏覽器引擎 - 查詢和操做渲染引擎的入口。
  3. 渲染引擎 - 負責呈現請求內容。例如請求內容是HTML, 渲染引擎負責解析HTML以及CSS,而且渲染解析後的內容到屏幕上。
  4. 網絡連接 - 處理形如HTTP的網絡請求。它有針對不一樣平臺的實現接口。
  5. 用戶界面的後臺處理程序(UI Backend) - 用於繪製相似於 bombo 盒子的小部件以及一些窗口。它拋出了各個平臺通用的接口。它的底層是
    使用了操做系統的用戶界面方法。
  6. Javascript 解釋器。 用於解析和執行Javascrip代碼。
  7. 數據存儲。 這是一個持久層。瀏覽器須要在硬盤上保存各類各樣的數據,好比coocies。HTNL5規範定義了'web database',針對瀏覽器的完整的數據庫(儘管比較輕量)數據庫

    瀏覽器主要組成部分

    Figure 1: Browser main components.

值得注意的是,Chrome不像其餘的瀏覽器, 它給每個tab分配一個渲染引擎的實例,每個tab都是一個獨立的進程。express

1.4 組件間的通訊

Firefox 和 Chrome 都獨自開發了一套特別的通訊機制。

2. 渲染引擎

渲染引擎的職責就是進行渲染, 也就是負責把請求到的內容呈如今瀏覽器屏幕上。

在默認狀況下,渲染引擎可以展現HTML,XML以及image文檔。也能經過插件來展現其餘類型的文檔。例如經過PDF視圖插件能夠展現PDF。咱們將會在特定的章節討論插件和擴展程序。本章節主要着重於主要的狀況-如何展現由css格式化的HTML和images。

2.1 渲染引擎

咱們參考的Firefox,Chrome,Safari瀏覽器都是基於兩個渲染引擎創建的。 Firefox 使用 Gecko, 一個Mozilla本身開發的引擎。Safari 和 Chrome 都是使用的Webkit引擎。Webkit 引擎最開始是用於linux平臺的開源引擎。後續被修改用於支持Apple的Mac以及 Windows系統。 詳情移步http://webkit.org/

2.2 主要渲染流程

渲染引擎將會從網絡層請求到內容開始進行工做。這一般的大小在8k之內。

在這以後,如下就是渲染引擎基本的流程:

渲染引擎基本流程

Figure 2: Render engine basic flow.

解析HTML, 生成DOM tree -> 渲染render tree結構 -> 組織render tree 的佈局 -> 在窗口繪製 render tree

渲染引擎會解析HTML文檔,把HTML文檔解析爲「內容樹(content tree)」, 並把HTML標籤轉換爲樹中的DOM節點。渲染引擎還要解析樣式文件,包含外鏈樣式文件以及內聯樣式元素。樣式信息和HTML當中可視化的指令將會用於建立另一個樹 - 渲染樹 (render tree)。

渲染樹包含了具備顏色以及尺寸等可視化屬性的矩形盒子集合。這些矩形盒子都是按照在屏幕上的顯示順序排序的。

在構造晚渲染樹以後,將會通過「layout」過程。意思就是給每個節點設置在屏幕上顯示的確切座標位置。下一個階段是繪製(painting) - 渲染樹將會經過UI的後臺處理層,每個節點都將會被繪製。

瞭解渲染的過程是一個按部就班的過程很重要。爲了達到更好的用戶體驗,渲染引擎將會盡量快的把內容展現在屏幕上。它並不會等到全部的HTML都解析完以後纔去構建和佈局渲染樹。當請求到一部份內容的時候,引擎將會解析和渲染這一部份內容,同時程序也將繼續解析從網絡中請求到的餘下的內容。

2.3 渲染流程示例

webkit flow

Figure 3: Webkit main flow

Gecko flow

Figure 4: Mozilla's Gecko rendering engine main flow

從圖3 和圖4 中能夠看到儘管Webkit 和 Gecko 使用了稍微不一樣的術語,可是流程是基本相同的。
Gecko 把格式化的元素形象的稱爲:Frame tree(結構樹)。每個元素都是一個框架。Webkit 使用術語:Render tree, 它由Render Object 組成。Webkit把設置元素的位置稱爲layout,而 Gecko稱爲Reflow。 Webkit 把鏈接DOM節點和視覺信息生成渲染樹稱爲Attachment。另一個較小的非語義上的差異是Gecko在HTML與DOM樹之間多了額外的一層。叫作content sink, 它是建立DOM元素的工廠。咱們將會逐個瞭解流程的每一部分。

一般的解析

既然解析在渲染引擎內是一個很是重要的過程,咱們將會深刻的瞭解它。
文檔解析,亦即把它轉換爲一種代碼能夠理解和使用的結構。解析的結果一般是一個表示文檔結構的節點樹。它被稱爲解析樹或者語法樹
例如:2 + 3 - 1 的表達式解析結果爲

運算表達式的樹節點

Figure 5: 運算表達式的樹節點

文法

解析是基於建立文檔語言所遵循的語法規則。每個你可以解析的格式,都有一個由詞法和語法規則組成的確切的文法。它被稱爲context free grammar(上下文無關的語法)。人類語言不是這樣的語言,也就是說無法用常規的解析技術來進行解析。

解析器 - 詞法組合

解析能夠被分爲兩個步驟 - 詞法分析 以及 語法分析。
詞法分析是把輸入的內容分解爲不少符號的一個過程。這些符號是構成語言的詞彙(構建語言有效的塊集合)。在人類的語言中,它就是某種語言在字典中的全部單詞所組成的。
語法分析就是語言語法規則的應用。
解析器的工做一般分爲兩個內容:詞法分析器(有時稱爲 標記生成器)負責把輸入分解爲不少符號,解析器負責根據該語言的語法規則來分析文檔結構,從而構建解析樹。詞法分析器知道如何區分和解釋特殊的字符,例如空格和換行符。
document to parse trees

Figure 6: from source document to parse trees

解析的過程是迭代式的。解析器一般會向詞法分析器詢問是否有新的符號,而且試圖經過一條語法規則的來進行匹配。若是符合某條語法規則,該符號對應的節點將會被添加到解析樹,緊接着解析器會詢問另一個符號就行解析。
若是沒有規則匹配,解析器會在內部存儲這個符號,並繼續詢問下一個符號直到某條規則匹配全部的內部存儲的符號。若是沒有找到對應的規則,解析器就回拋出一個異常。這意味着這個文檔無效,而且包含語法錯誤。

翻譯

一般解析樹並非最終的結果。解析結果一般被翻譯-把文檔翻譯爲另一種格式。一個例子就是彙編。編譯器會把源碼編譯爲機器碼,首先會把源碼解析爲解析樹,而後再把解析樹翻譯爲機器碼文檔。
compilation flow

Figure 7: compilation flow

解析實例

在圖5中,咱們從一個數學表達式中建立了一個解析樹。 讓咱們來定義一個簡單的數學語言來了解解析過程。

詞彙: 咱們的語言包含整數,加法符號,減法符號

語法:

1. 構成語法的元素包含表達式,運算項,運算符。  
2. 咱們的語言可以包含任意數量的表達式。  
3. 一個表達式定義爲: 一個運算項 跟着一個 操做符,再跟着另一個運算項。  
4. 操做符爲加號或者減號  
5. 運算項爲一個整數或者一個表達式。

分析下:"2 + 3 - 1"。
根據上面第5條規則,第一個匹配規則的子串是"2"。第二個匹配的的結果是"2 + 3",它對應第二條規則。下一個匹配的結果已經到了該輸入項的結尾。咱們已經知道了形如?2 + 3?表示一個完整項,那麼 "2 + 3 + 1"就是一個表達式。"2 + +" 是一個無效的輸入,由於沒有匹配任何規則。

正式的定義詞彙和語法

詞彙一般都經過常規的表達式來表示。
例如咱們將會像下面這樣來定義咱們的語言:
INTER :0|1-9*
PLUS : +
MINUS : -
如你所見,整數是經過常規的表達式來表達的。
語法是遵循BNF(Backus Naur form).html)來定義的。咱們的語言將會作以下的定義:
expression := term operation tem
operation := PLUS | MINUS
term := INTEGER | expression

咱們以前說過,若是程序的語法是一個上下文無關的語法,就可使用一般的解析器進行解析。上下文無關的語法,最直觀的定義就是能夠徹底使用BNF來表示。能夠參見http://en.wikipedia.org/wiki/Context-free_grammar

解析器類型

解析器有兩種類型: 自上而下 和 自下而上 的解析器。 自上而下的解析器是從語法層級比較高的地方着手進行匹配解析。自下而上的解析方式是從輸入開始,逐級向上翻譯爲對應的語法規則,直到語法層級較高的規則爲止。

讓咱們結合實例來看看這兩種解析方式:

自上而下的解析將會從層級比較高的規則開始: 它將把 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 parser),由於輸入是從左向右移動的,而且根據規則匹配主鍵減小。

自動生成解析器

能夠經過工具生成解析器,被稱之爲解析器生成器。 你只須要提供語言的詞彙以及語法規則,它就可以生成一個可用的解析器。建立一個解析器徐傲對解析有深刻的理解。不太容易手動建立一個解析器,全部解析器生成器會比較有用。

Webkit 使用兩個比較出名的解析器生成器: Flex 用於建立詞法分析器, Bison用於建立解析器(你可使用Lex 和 Yacc來運行)。Flex的輸入是包含一般的表達式定義的一個文件。Bison 的輸入是BNF格式的語法規則。

HTML 解析器

HTML解析器的職責是把HTML標記轉換爲解析樹。

HTML 語法定義

HTML的詞彙和語法在w3c建立的規範裏定義。

非上下文無關的程序語言

在解析一節的介紹裏,咱們知道程序語法能夠經過BNF格式進行定義。
可是不幸的是,全部常規的解析器都不適用於HTML。HTML不可以被輕易的定義爲解析器須要的上下文無關的程序語法。
有一個定義HTML的通用格式-DTD(Document Type Definition), 不過並非上下文無關的語法。
一眼看上去, HTML與XML很是的接近。 有不少的XML解析器。有一個HTML的XML變體-XXHTML。這兩者有什麼不用呢?
不一樣之處在於HTML的目的在於非嚴謹的,它容許忽略你某些標籤,並隱式的添加上,例若有時候容許忽略開始或者結束標籤。不一樣於XML語法的嚴格和硬性要求,HTML總體上都是比較寬泛的。
一方面這也是HTML如此瀏覽的一個緣由,容許你犯錯,讓web開發更容易。另外一方面,它致使很難定義一個語法格式。總結起來講, HTML比較難解析,因爲並非一個上下文無關的編程語法,它不可以被普通的解析器解析, 也不能被XML解析器解析。

HTML DTD

HTML是經過DTD來定義的。 這個格式用於定義SGML(Standard Generalized Markup Language)語言。 它定義了全部容許的元素,屬性以及層級。正如咱們以前所說的, HTML DTD 不能造成上下文無關的語言。
DTD有一些變更,嚴格模式嚴格符合規範,其餘的模式支持歷史版本的瀏覽器。 目的也是爲了兼容老版本的瀏覽器。 最新的嚴格DTD地址:http://www.w3.org/TR/html4/strict.dtd

DOM

解析樹是由DOM元素以及屬性節點組成的。 DOM是Document Objectd Model 的簡稱。 它是HTML文檔的對象形式以及其餘外部語言(形如Javascript)的接口。樹的根節點是 Document 對象。

DOM與標籤以前有着一對一的對應關係。 例如:

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

將會被翻譯爲如下的DOM樹:

Figure 8: DOM tree of the example markup

跟HTML同樣, DOM也是被w3c組織定義和管理的。詳見http://www.w3.org/DOM/DOMTR。 它是操做文檔的通用規範。 一個特定的模塊描述了HTML特定的元素。 HTML定義能夠參見http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html

當我說樹包含了DOM節點, 意即樹是由實現了DOM接口的元素構建的。 不一樣的瀏覽器使用了具體的實現,這些實現包含了瀏覽器內部使用的其餘屬性。

解析算法

在前幾節中,咱們知道, HTML不可以經過自上而下或者自下而上的解析器解析。
緣由以下:

1. 語言的非嚴謹性。  
2. 瀏覽器具備傳統的容錯機制,用於支持很好的辨別無效的HTML。  
3. 解析進程的反覆迭代機制。 在解析的過程當中,解析源一般都不會被改變, 可是在HTML中, script標籤包含了document.write, 能夠添    加額外的元素,因此解析過程當中修改了原始輸入。

因爲不可以使用一般的解析器就行解析, 瀏覽器爲解析HTML建立了定製的解析器。
解析算法在HTML5規範中有詳細的描述。 算法由兩步構成: 符號化 和 構建樹。
符號化即詞法分析,把輸入解析爲一組符號。 HTML的符號包括開始標籤, 結束標籤, 屬性名和屬性值。
標記生成器識別不一樣的標記, 並把它傳遞給樹構造器,緊接着識別下一個標記, 周而復始, 直到結束。

Figure 6: HTML parsing flow (taken from HTML5 spec)

符號化的算法

算法的輸出結果是一個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>"標籤也以一樣的方式進行處理。到目前爲止,"html"和"body"標籤都被觸發。咱們如今回到了"Data state"。

處理"Hello world"中的"H"字符會建立和出發一個字符標籤,直到遇到"</bodu>"的"<"符號爲止。咱們會爲"Hello world"的每個字符都觸發一個字符標籤。

如今咱們回到"Tag open state"。處理"/"會建立一個"end tag token",而且狀態變動爲"Tag name state"。咱們依然保留當前狀態直到遇到">"爲止。以後新的標籤就回被出發,狀態返回"Data state"。"</html>"的處理方式雷同。

Tokenizing the example input

Figure 9: Tokenizing the example input

樹結構算法

當解析器被建立的時候,文檔對象也會被建立。在構建樹結構的過程當中,文檔的DOM樹將會被修改,相應的元素將會被添加進去。標記生成器建立的每個節點都會被樹構造器處理。規範中定義的每個DOM元素關聯的標記都會被建立。除了把元素添加到DOM樹以外,還會被添加到"open elements"棧中。這個棧被用於糾正不匹配的嵌套以及處理未關閉的標籤。這個算法過程也被描述爲一個狀態機。狀態被稱爲"insertion modes"。

讓咱們看看事例中構造樹的過程:

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

樹構造階段接收的輸入是從字符化階段傳入的字符序列。第一個模式是"initial mode"。當接收到html標籤的時候,會移動到"before html"模式,同時再對標籤進行處理。此時會建立一個HTMLHtmlElement元素,這個元素會被添加到文檔對象的根節點。

以後狀態將會變爲"before head"。咱們會接收到body標籤,此時將會隱式的建立一個HTMLHeadElementut元素並添加到DOM樹裏,儘管示例中並無head標籤。

緊接着移動到"in head",而後是"after head"。body標籤會被再加工,一個HTMLBodyElement將會被建立和添加到DOM樹, 模式會移動到"in body"。

接下來會接收到"Hello world"字符串。處理第一個字符的時候會建立一個"Text"節點,其餘的字符會被添加到這個節點中。

當接收到body結束標籤的時候會移動到"after body"模式。此時咱們會接收到html結束標籤,會移動到"after after body"模式。接收到文件結束標籤的時候將會結束解析。

tree construction of example html

Figure 10: tree construction of example html

解析以後的動做

在這一步瀏覽器將會把文檔標記爲可交互的,同時開始解析在「defferred」模式下的scripts文件(在文檔解析完成以後將會被執行)。文檔的狀態將會被修改成「complete」,同時觸發一個「load」事件。

你能夠在HTML5規範裏查看標記化以及構建樹的完整算法。https://www.w3.org/TR/html5/syntax.html

瀏覽器的容錯

在HTML頁面裏你永遠不會收到一個「語法無效」的錯誤。瀏覽器會處理無效的內容。

如下面的HTML爲例:

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

我已經違反了不少規則(「mytag」不是標準的標籤,「p」和「div」標籤的錯誤嵌套等等),可是瀏覽器仍然會把內容正確的展現出來,並不會報錯。因此有不少的解析代碼來修復了HTML開發者的錯誤。

瀏覽器中的錯誤處理始終是一致的,可是讓人比較驚訝的是它並非當前HTML規範的一部分。它就像書籤和前進後退按鈕同樣,只是多年以來在瀏覽器中開發出來的某個東西。 在不少站點中都已知不少無效的HTML結構,瀏覽器會試着已一致的方式修復它們,以順應其餘瀏覽器。

HTML5規範針對這些要求作了一些定義。Webkit在HTML解析類開始的註釋中作了很好的描述:

The parser parses tokenized input into the document, building up the document tree. If the document is well-formed, parsing it is straightforward.

Unfortunately, we have to handle many HTML documents that are not well-formed, so the parser has to be tolerant about errors.

We have to take care of at least the following error conditions:

1. The element being added is explicitly forbidden inside some outer tag.
In this case we should close all tags up to the one, which forbids the element, and add it afterwards.

2. We are not allowed to add the element directly. 
It could be that the person writing the document forgot some tag in between (or that the tag in between is optional).
This could be the case with the following tags: HTML HEAD BODY TBODY TR TD LI (did I forget any?).

3. We want to add a block element inside to an inline element. Close all inline elements up to the next higher block element.

4. If this doesn't help, close elements until we are allowed to add the element or ignore the tag.

讓咱們看看Webkit的容錯示例:

</br> instead of <br>

某些站點使用</br>代替<br>。爲了兼容IE和Firefox, Webkit 使用<br>。

代碼:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
    reportError(MalformedBRError);
    t->beginTag = true;
}

注意: 錯誤處理是在內容,並不會呈現給用戶。

錯亂的table

錯亂偏離的table是指在另一個table裏可是卻不在table cell裏的table。

就像下面的例子:

<table>
    <table>
        <tr><td>inner table</td></tr>
        </table>
    <tr><td>outer table</td></tr>
</table>

Webkit 就會把結構修改成兩個子table

<table>
    <tr><td>outer table</td></tr>
</table>
<table>
    <tr><td>inner table</td></tr>
</table>

代碼:

if (m_inStrayTableContent && localName == tableTag)
    popBlock(tableTag);

Webkit 使用棧來管理當前元素內容,它會彈出內部table,再入棧到外部table的棧中。table至此就相鄰了。

嵌套的表單元素

以防用戶在form 中放置另一個form, 第二個form將會被忽略。

if (!m_currentFormElement) {
    m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

過深的標籤層級

註釋不言而喻。

www.liceo.edu.mx is an example of a site that achieves a level of nesting of about 1500 tags, all from a bunch of <b>s.
We will only allow at most 20 nested tags of the same type before just ignoring them all together.

代碼:

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

看註釋:

Support for really broken html.
We never close the body tag, since some stupid web pages close it before the actual end of the doc.
Let's rely on the end() call to close things.

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

因此web開發者須要注意: 除非你想呈現一個Webkit容錯的示例代碼,不然請編寫完整的HTML標籤。

CSS解析

待續...

相關文章
相關標籤/搜索