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

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

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" 時,會創建和釋放一個字符標記。這個過程持續到遇到 "" 的 "

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


分享到:


相關文章: