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

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

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 和 標記有著幾乎一對一的關係。例如:



Hello World.





上面內容會被轉換成:

像 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:



Hello world.


初始的狀態是 "數據狀態"。當遇到 "" 這個字符時,這個狀態才會結束。每個字符都被添加到這個標記名稱下。在我們的例子中,創建的標記就是 "html"。

當遇到 ">" 時,當前的標記會被釋放,然後狀態變回 "數據狀態"。"

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

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