1. 程式人生 > >[譯]瀏覽器工作原理探究

[譯]瀏覽器工作原理探究

引言

最近對web的效能優化比較感興趣,而前端程式碼主要在瀏覽器工作的。如果對瀏覽器的工作原理了解清楚,可以為web效能優化提供方向以及理論依據。
本文主要參考 How Browsers Work: Behind the scenes of modern web browsers 。在此基礎上整理而來的。個人覺得這是一篇好文章,所以進行整理轉載,希望對大家有所幫助。(文章篇幅比較長,需花點時間^_^)

目錄

簡介

我們要討論的瀏覽器

目前使用的主流瀏覽器有五個:Internet Explorer、Firefox、Safari、Chrome 瀏覽器和 Opera。本文中以開放原始碼瀏覽器為例,即 Firefox、Chrome 瀏覽器和 Safari(部分開源)。

瀏覽器的主要功能

瀏覽器的主要功能就是向伺服器發出請求,在瀏覽器視窗中展示您選擇的網路資源。這裡所說的資源一般是指 HTML 文件,也可以是 PDF、圖片或其他的型別。資源的位置由使用者使用 URI(統一資源標示符)指定。

瀏覽器解釋並顯示 HTML 檔案的方式是在 HTML 和 CSS 規範中指定的。這些規範由網路標準化組織 W3C(全球資訊網聯盟)進行維護。 多年以來,各瀏覽器都沒有完全遵從這些規範,同時還在開發自己獨有的擴充套件程式,這給網路開發人員帶來了嚴重的相容性問題。如今,大多數的瀏覽器都是或多或少地遵從規範。

瀏覽器的使用者介面有很多彼此相同的元素,其中包括:

  • 用來輸入 URI 的位址列
  • 前進和後退按鈕
  • 書籤設定選項
  • 用於重新整理和停止載入當前文件的重新整理和停止按鈕
  • 用於返回主頁的主頁按鈕

瀏覽器的高層結構

瀏覽器的主要元件為:

  1. 使用者介面 - 包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主視窗顯示的您請求的頁面外,其他顯示的各個部分都屬於使用者介面。
  2. 瀏覽器引擎 - 在使用者介面和渲染引擎之間傳送指令。
  3. 渲染引擎 - 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在螢幕上。
  4. 網路 - 用於網路呼叫,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
  5. 使用者介面後端 - 用於繪製基本的視窗小部件,比如組合框和視窗。其公開了與平臺無關的通用介面,而在底層使用作業系統的使用者介面方法。
  6. JavaScript 直譯器。用於解析和執行 JavaScript 程式碼。
  7. 資料儲存。這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookie。新的 HTML 規範 (HTML5) 定義了“網路資料庫”,這是一個完整(但是輕便)的瀏覽器內資料庫。

瀏覽器元件

圖1: 瀏覽器元件

渲染引擎

渲染引擎主要用來展示HTML,XML,圖片,PDF等資源。本文所討論的瀏覽器(Firefox、Chrome 瀏覽器和 Safari)是基於兩種渲染引擎構建的。Firefox 使用的是 Gecko,這是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome 瀏覽器使用的都是 WebKit。

WebKit 是一種開放原始碼渲染引擎,起初用於 Linux 平臺,隨後由 Apple 公司進行修改,從而支援蘋果機和 Windows。

主要流程

渲染引擎一開始會從網路層獲取請求文件的內容,內容的大小一般限制在 8000 個塊以內。

然後進行如下所示的基本流程:

渲染引擎工作流程

圖2: 渲染引擎工作流程

渲染引擎將開始解析 HTML 文件,並將各標記逐個轉化成“內容樹”上的 DOM 節點。同時也會解析外部 CSS 檔案以及樣式元素中的樣式資料。HTML 中這些帶有視覺指令的樣式資訊將用於建立另一個樹結構:渲染樹。

渲染樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在螢幕上顯示的順序。

渲染樹構建完畢之後,進入“佈局”處理階段,也就是為每個節點分配一個應出現在螢幕上的確切座標。下一個階段是繪製 - 渲染引擎會遍歷渲染樹,由使用者介面後端層將每個節點繪製出來。

需要著重指出的是,這是一個漸進的過程。為達到更好的使用者體驗,渲染引擎會力求儘快將內容顯示在螢幕上。它不必等到整個 HTML 文件解析完畢之後,就會開始構建渲染樹和設定佈局。在不斷接收和處理來自網路的其餘內容的同時,渲染引擎會將部分內容解析並顯示出來。

主要流程如下:

WebKit主要流程

圖3: WebKit主要流程

Gecko主要流程

圖4: Gecko主要流程

從圖 3 和圖 4 可以看出,雖然 WebKit 和 Gecko 使用的術語略有不同,但整體流程是基本相同的。

Gecko 將視覺格式化元素組成的樹稱為“框架樹”。每個元素都是一個框架。WebKit 使用的術語是“渲染樹”,它由“渲染物件”組成。對於元素的放置,WebKit 使用的術語是“佈局”,而 Gecko 稱之為“重排”。對於連線 DOM 節點和視覺化資訊從而建立渲染樹的過程,WebKit 使用的術語是“附加”。有一個細微的非語義差別,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱為“內容槽”的層,用於生成 DOM 元素。我們會逐一論述流程中的每一部分:

解析和DOM樹構建

解析 - 綜述

解析是渲染引擎中非常重要的一個環節,因此我們要更深入地講解。首先,來介紹一下解析。

解析文件是指將文件轉化成為有意義的結構,也就是可讓程式碼理解和使用的結構。解析得到的結果通常是代表了文件結構的節點樹,它稱作解析樹或者語法樹。

示例 - 解析 2 + 3 - 1 這個表示式,會返回下面的樹:

數學表示式樹節點

圖5: 數學表示式樹節點
語法

解析是以文件所遵循的語法規則(編寫文件所用的語言或格式)為基礎的。所有可以解析的格式都必須對應確定的語法(由詞彙和語法規則構成)。這稱為與上下文無關的語法。人類語言並不屬於這樣的語言,因此無法用常規的解析技術進行解析。

解析器和詞法分析器的組合

解析的過程可以分成兩個子過程:詞法分析和語法分析。

詞法分析是將輸入內容分割成大量標記的過程。標記是語言中的詞彙,即構成內容的單位。在人類語言中,它相當於語言字典中的單詞。

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

解析器通常將解析工作分給以下兩個元件來處理:詞法分析器(有時也稱為標記生成器),負責將輸入內容分解成一個個有效標記;而解析器負責根據語言的語法規則分析文件的結構,從而構建解析樹。詞法分析器知道如何將無關的字元(比如空格和換行符)分離出來。

從源文件到解析樹

圖6: 從源文件到解析樹

解析是一個迭代的過程。通常,解析器會向詞法分析器請求一個新標記,並嘗試將其與某條語法規則進行匹配。如果發現了匹配規則,解析器會將一個對應於該標記的節點新增到解析樹中,然後繼續請求下一個標記。

如果沒有規則可以匹配,解析器就會將標記儲存到內部,並繼續請求標記,直至找到可與所有內部儲存的標記匹配的規則。如果找不到任何匹配規則,解析器就會引發一個異常。這意味著文件無效,包含語法錯誤。

翻譯

很多時候,解析樹還不是最終產品。解析通常是在翻譯過程中使用的,而翻譯是指將輸入文件轉換成另一種格式。編譯就是這樣一個例子。編譯器可將原始碼編譯成機器程式碼,具體過程是首先將原始碼解析成解析樹,然後將解析樹翻譯成機器程式碼文件。

編譯流程

圖7: 編譯流程
解析示例

在圖 5 中,我們通過一個數學表示式建立了解析樹。現在,讓我們試著定義一個簡單的數學語言,用來演示解析的過程。

詞彙:我們用的語言可包含整數、加號和減號。

語法:

構成語言的語法單位是表示式、項和運算子。 我們用的語言可以包含任意數量的表示式。 表示式的定義是:一個“項”接一個“運算子”,然後再接一個“項”。 運算子是加號或減號。 項是一個整數或一個表示式。 讓我們分析一下 2 + 3 - 1。 匹配語法規則的第一個子串是 2,而根據第 5 條語法規則,這是一個項。匹配語法規則的第二個子串是 2 + 3,而根據第 3 條規則(一個項接一個運算子,然後再接一個項),這是一個表示式。下一個匹配項已經到了輸入的結束。2 + 3 - 1 是一個表示式,因為我們已經知道 2 + 3 是一個項,這樣就符合“一個項接一個運算子,然後再接一個項”的規則。2 + + 不與任何規則匹配,因此是無效的輸入。

詞彙和語法的正式定義

詞彙通常用正則表示式表示。

例如,我們的示例語言可以定義如下:

    INTEGER :0|[1-9][0-9]*
    PLUS : +
    MINUS: -
複製程式碼

正如您所看到的,這裡用正則表示式給出了整數的定義。

語法通常使用一種稱為 BNF 的格式來定義。我們的示例語言可以定義如下:

    expression :=  term  operation  term
    operation :=  PLUS | MINUS
    term := INTEGER | expression
複製程式碼

之前我們說過,如果語言的語法是與上下文無關的語法,就可以由常規解析器進行解析。與上下文無關的語法的直觀定義就是可以完全用 BNF 格式表達的語法。

解析器型別

有兩種基本型別的解析器:自上而下解析器和自下而上解析器。直觀地來說,自上而下的解析器從語法的高層結構出發,嘗試從中找到匹配的結構。而自下而上的解析器從低層規則出發,將輸入內容逐步轉化為語法規則,直至滿足高層規則。

讓我們來看看這兩種解析器會如何解析我們的示例:

自上而下的解析器會從高層的規則開始:首先將 2 + 3 標識為一個表示式,然後將 2 + 3 - 1 標識為一個表示式(標識表示式的過程涉及到匹配其他規則,但是起點是最高級別的規則)。

自下而上的解析器將掃描輸入內容,找到匹配的規則後,將匹配的輸入內容替換成規則。如此繼續替換,直到輸入內容的結尾。部分匹配的表示式儲存在解析器的堆疊中。

堆疊 輸入
2 + 3 - 1
+ 3 - 1
項運算 3 - 1
表示式 - 1
表示式運算子 1
表示式

這種自下而上的解析器稱為移位歸約解析器,因為輸入在向右移位(設想有一個指標從輸入內容的開頭移動到結尾),並且逐漸歸約到語法規則上。

自動生成解析器

有一些工具可以幫助您生成解析器,它們稱為解析器生成器。您只要向其提供您所用語言的語法(詞彙和語法規則),它就會生成相應的解析器。建立解析器需要對解析有深刻理解,而人工建立並優化解析器並不是一件容易的事情,所以解析器生成器是非常實用的。

WebKit 使用了兩種非常有名的解析器生成器:用於建立詞法分析器的 Flex 以及用於建立解析器的 Bison(您也可能遇到 Lex 和 Yacc 這樣的別名)。Flex 的輸入是包含標記的正則表示式定義的檔案。Bison 的輸入是採用 BNF 格式的語言語法規則。

HTML解析器

HTML 解析器的任務是將 HTML 標記解析成解析樹。

HTML語法定義

HTML 的詞彙和語法在 W3C 組織建立的規範中進行了定義。當前的版本是 HTML4,HTML5 正在處理過程中。

非與上下文無關語法

正如我們在解析過程的簡介中已經瞭解到的,語法可以用 BNF 等格式進行正式定義。

很遺憾,所有的常規解析器都不適用於 HTML(我並不是開玩笑,它們可以用於解析 CSS 和 JavaScript)。HTML 並不能很容易地用解析器所需的與上下文無關的語法來定義。

有一種可以定義 HTML 的正規格式:DTD(Document Type Definition,文件型別定義),但它不是與上下文無關的語法。

這初看起來很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一個 XML 變體 (XHTML),那麼有什麼大的區別呢?

區別在於 HTML 的處理更為“寬容”,它允許您省略某些隱式新增的標記,有時還能省略一些起始或者結束標記等等。和 XML 嚴格的語法不同,HTML 整體來看是一種“軟性”的語法。

顯然,這種看上去細微的差別實際上卻帶來了巨大的影響。一方面,這是 HTML 如此流行的原因:它能包容您的錯誤,簡化網路開發。另一方面,這使得它很難編寫正式的語法。概括地說,HTML 無法很容易地通過常規解析器解析(因為它的語法不是與上下文無關的語法),也無法通過 XML 解析器來解析。

HTML DTD

HTML 的定義採用了 DTD 格式。此格式可用於定義 SGML 族的語言。它包括所有允許使用的元素及其屬性和層次結構的定義。如上文所述,HTML DTD 無法構成與上下文無關的語法。

DTD 存在一些變體。嚴格模式完全遵守 HTML 規範,而其他模式可支援以前的瀏覽器所使用的標記。這樣做的目的是確保向下相容一些早期版本的內容。最新的嚴格模式 DTD 可以在這裡找到:www.w3.org/TR/html4/st…

DOM

解析器的輸出“解析樹”是由 DOM 元素和屬性節點構成的樹結構。DOM 是文件物件模型 (Document Object Model) 的縮寫。它是 HTML 文件的物件表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的介面。 解析樹的根節點是“Document”物件。

DOM 與標記之間幾乎是一一對應的關係。比如下面這段標記:

    <html>
    <body>
        <p>
        Hello World
        </p>
        <div> <img src="example.png"/></div>
    </body>
    </html>
複製程式碼

可翻譯成如下的 DOM 樹:

示例標記的 DOM 樹

圖8: 示例標記的 DOM 樹

和 HTML 一樣,DOM 也是由 W3C 組織指定的。請參見 www.w3.org/DOM/DOMTR。這… HTML 的元素。HTML 的定義可以在這裡找到:www.w3.org/TR/2003/REC…

這裡的樹包含 DOM 節點,指的是樹是由實現了某個 DOM 介面的元素構成的。瀏覽器在具體的實現中會有一些供內部使用的其他屬性。

解析演算法

我們在之前章節已經說過,HTML 無法用常規的自上而下或自下而上的解析器進行解析。

原因在於:

  1. 語言的寬容本質。
  2. 瀏覽器歷來對一些常見的無效 HTML 用法採取包容態度。
  3. 解析過程需要不斷地反覆。源內容在解析過程中通常不會改變,但是在 HTML 中,指令碼標記如果包含 document.write,就會新增額外的標記,這樣解析過程實際上就更改了輸入內容。

由於不能使用常規的解析技術,瀏覽器就建立了自定義的解析器來解析 HTML。

HTML5 規範詳細地描述瞭解析演算法。此演算法由兩個階段組成:標記化和樹構建。

標記化是詞法分析過程,將輸入內容解析成多個標記。HTML 標記包括起始標記、結束標記、屬性名稱和屬性值。

標記生成器識別標記,傳遞給樹構造器,然後接受下一個字元以識別下一個標記;如此反覆直到輸入的結束。

HTML 解析流程

圖9: HTML 解析流程
標記化演算法

該演算法的輸出結果是 HTML 標記。該演算法使用狀態機來表示。每一個狀態接收來自輸入資訊流的一個或多個字元,並根據這些字元更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。這意味著,即使接收的字元相同,對於下一個正確的狀態也會產生不同的結果,具體取決於當前的狀態。該演算法相當複雜,無法在此詳述,所以我們通過一個簡單的示例來幫助大家理解其原理。

基本示例 - 將下面的 HTML 程式碼標記化:

    <html>
    <body>
        Hello world
    </body>
    </html>
複製程式碼

初始狀態是資料狀態。遇到字元 < 時,狀態更改為“標記開啟狀態”。接收一個 a-z 字元會建立“起始標記”,狀態更改為“標記名稱狀態”。這個狀態會一直保持到接收 > 字元。在此期間接收的每個字元都會附加到新的標記名稱上。在本例中,我們建立的標記是 html 標記。

遇到 > 標記時,會發送當前的標記,狀態改回“資料狀態”。 標記也會進行同樣的處理。目前 html 和 body 標記均已發出。現在我們回到“資料狀態”。接收到 Hello world 中的 H 字元時,將建立併發送字元標記,直到接收 中的 <。我們將為 Hello world 中的每個字元都發送一個字元標記。

現在我們回到“標記開啟狀態”。接收下一個輸入字元 / 時,會建立 end tag token 並改為“標記名稱狀態”。我們會再次保持這個狀態,直到接收 >。然後將傳送新的標記,並回到“資料狀態”。 輸入也會進行同樣的處理。

對示例輸入進行標記化

圖10: 對示例輸入進行標記化
樹構建演算法

在建立解析器的同時,也會建立 Document 物件。在樹構建階段,以 Document 為根節點的 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”模式。接收到檔案結束標記後,解析過程就此結束。

示例 HTML 的樹構建

圖11: 示例 HTML 的樹構建
解析結束後的操作

在此階段,瀏覽器會將文件標註為互動狀態,並開始解析那些處於“deferred”模式的指令碼,也就是那些應在文件解析完成後才執行的指令碼。然後,文件狀態將設定為“完成”,一個“載入”事件將隨之觸發。

您可以在 HTML5 規範中檢視標記化和樹構建的完整演算法

瀏覽器的容錯機制

您在瀏覽 HTML 網頁時從來不會看到“語法無效”的錯誤。這是因為瀏覽器會糾正任何無效內容,然後繼續工作。

以下面的 HTML 程式碼為例:

    <html>
    <mytag>
    </mytag>
    <div>
    <p>
    </div>
        Really lousy HTML
    </p>
    </html>
複製程式碼

在這裡,已經違反了很多語法規則(“mytag”不是標準的標記,“p”和“div”元素之間的巢狀有誤等等),但是瀏覽器仍然會正確地顯示這些內容,並且毫無怨言。因為有大量的解析器程式碼會糾正 HTML 網頁作者的錯誤。

不同瀏覽器的錯誤處理機制相當一致,但令人稱奇的是,這種機制並不是 HTML 當前規範的一部分。和書籤管理以及前進/後退按鈕一樣,它也是瀏覽器在多年發展中的產物。很多網站都普遍存在著一些已知的無效 HTML 結構,每一種瀏覽器都會嘗試通過和其他瀏覽器一樣的方式來修復這些無效結構。

HTML5 規範定義了一部分這樣的要求。WebKit 在 HTML 解析器類的開頭註釋中對此做了很好的概括。

解析器對標記化輸入內容進行解析,以構建文件樹。如果文件的格式正確,就直接進行解析。

遺憾的是,我們不得不處理很多格式錯誤的 HTML 文件,所以解析器必須具備一定的容錯性。

我們至少要能夠處理以下錯誤情況:

  1. 明顯不能在某些外部標記中新增的元素。在此情況下,我們應該關閉所有標記,直到出現禁止新增的元素,然後再加入該元素。
  2. 我們不能直接新增的元素。這很可能是網頁作者忘記添加了其中的一些標記(或者其中的標記是可選的)。這些標籤可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
  3. 向 inline 元素內新增 block 元素。關閉所有 inline 元素,直到出現下一個較高階的 block 元素。
  4. 如果這樣仍然無效,可關閉所有元素,直到可以新增元素為止,或者忽略該標記。

讓我們看一些 WebKit 容錯的示例:

  • 使用了
    而不是
    有些網站使用了
    而不是
    。為了與 IE 和 Firefox 相容,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);
}
複製程式碼
  • 過於複雜的標記層次結構 程式碼的註釋已經說得很清楚了。
示例網站 www.liceo.edu.mx 嵌套了約 1500 個標記,全都來自一堆 標記。我們只允許最多 20 層同類型標記的巢狀,如果再巢狀更多,就會全部忽略。
  • 放錯位置的 html 或者 body 結束標記

同樣,程式碼的註釋已經說得很清楚了。

支援格式非常糟糕的 HTML 程式碼。我們從不關閉 body 標記,因為一些愚蠢的網頁會在實際文件結束之前就關閉。我們通過呼叫 end() 來執行關閉操作。
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;
複製程式碼

所以網頁作者需要注意,除非您想作為反面教材出現在 WebKit 容錯程式碼段的示例中,否則還請編寫格式正確的 HTML 程式碼。

CSS解析

還記得簡介中解析的概念嗎?和 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”是識別符號 (identifier) 的縮寫,比如類名。“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 表示空格)分隔的多個(數量可選)選擇器。規則集包含了大括號,以及其中的一個或多個(數量可選)由分號分隔的宣告。“宣告”和“選擇器”將由下面的 BNF 格式定義。

WebKit CSS解析器

WebKit 使用 Flex 和 Bison 解析器生成器,通過 CSS 語法檔案自動建立解析器。正如我們之前在解析器簡介中所說,Bison 會建立自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。這兩種解析器都會將 CSS 檔案解析成 styleSheet 物件,且每個物件都包含 CSS 規則。CSS 規則物件則包含選擇器和宣告物件,以及其他與 CSS 語法對應的物件。

解析 CSS

圖12: 解析 CSS

處理指令碼和樣式表的順序

指令碼

網路的模型是同步的。網頁作者希望解析器遇到 script 標記時立即解析並執行指令碼。文件的解析將停止,直到指令碼執行完畢。如果指令碼是外部的,那麼解析過程會停止,直到從網路同步抓取資源完成後再繼續。此模型已經使用了多年,也在 HTML4 和 HTML5 規範中進行了指定。作者也可以將指令碼標註為“defer”,這樣它就不會停止文件解析,而是等到解析結束才執行。HTML5 增加了一個選項,可將指令碼標記為非同步,以便由其他執行緒解析和執行。

預解析

WebKit 和 Firefox 都進行了這項優化。在執行指令碼時,其他執行緒會解析文件的其餘部分,找出並載入需要通過網路載入的其他資源。通過這種方式,資源可以在並行連線上載入,從而提高總體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工作交由主解析器處理;預解析器只會解析外部資源(例如外部指令碼、樣式表和圖片)的引用。

樣式表

另一方面,樣式表有著不同的模型。理論上來說,應用樣式表不會更改 DOM 樹,因此似乎沒有必要等待樣式表並停止文件解析。但這涉及到一個問題,就是指令碼在文件解析階段會請求樣式資訊。如果當時還沒有載入和解析樣式,指令碼就會獲得錯誤的回覆,這樣顯然會產生很多問題。這看上去是一個非典型案例,但事實上非常普遍。Firefox 在樣式表載入和解析的過程中,會禁止所有指令碼。而對於 WebKit 而言,僅當指令碼嘗試訪問的樣式屬性可能受尚未載入的樣式表影響時,它才會禁止該指令碼。

渲染樹構建

在 DOM 樹構建的同時,瀏覽器還會構建另一個樹結構:渲染樹。這是由視覺化元素按照其顯示順序而組成的樹,也是文件的視覺化表示。它的作用是讓您按照正確的順序繪製內容。

Firefox 將渲染樹中的元素稱為“框架”。WebKit 使用的術語是渲染器或渲染物件。 渲染器知道如何佈局並將自身及其子元素繪製出來。 WebKits RenderObject 類是所有渲染器的基類,其定義如下:

    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 屬性的不同,針對同一個 DOM 節點應建立什麼型別的渲染器。

    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樹的關係

渲染器是和 DOM 元素相對應的,但並非一一對應。非視覺化的 DOM 元素不會插入渲染樹中,例如“head”元素。如果元素的 display 屬性值為“none”,那麼也不會顯示在渲染樹中(但是 visibility 屬性值為“hidden”的元素仍會顯示)。

有一些 DOM 元素對應多個視覺化物件。它們往往是具有複雜結構的元素,無法用單一的矩形來描述。例如,“select”元素有 3 個渲染器:一個用於顯示區域,一個用於下拉列表框,還有一個用於按鈕。如果由於寬度不夠,文字無法在一行中顯示而分為多行,那麼新的行也會作為新的渲染器而新增。

另一個關於多渲染器的例子是格式無效的 HTML。根據 CSS 規範,inline 元素只能包含 block 元素或 inline 元素中的一種。如果出現了混合內容,則應建立匿名的 block 渲染器,以包裹 inline 元素。

有一些渲染物件對應於 DOM 節點,但在樹中所在的位置與 DOM 節點不同。浮動定位和絕對定位的元素就是這樣,它們處於正常的流程之外,放置在樹中的其他地方,並對映到真正的框架,而放在原位的是佔位框架。

渲染樹及其對應的 DOM 樹。初始容器 block 為“viewport”,而在 WebKit 中則為“RenderView”物件。

圖13: 渲染樹及其對應的 DOM 樹。初始容器 block 為“viewport”,而在 WebKit 中則為“RenderView”物件。

構建渲染樹的流程

在 Firefox 中,系統會針對 DOM 更新註冊展示層,作為偵聽器。展示層將框架建立工作委託給 FrameConstructor,由該構造器解析樣式(請參閱樣式計算)並建立框架。

在 WebKit 中,解析樣式和建立渲染器的過程稱為“附加”。每個 DOM 節點都有一個“attach”方法。附加是同步進行的,將節點插入 DOM 樹需要呼叫新的節點“attach”方法。

處理 html 和 body 標記就會構建渲染樹根節點。這個根節點渲染物件對應於 CSS 規範中所說的容器 block,這是最上層的 block,包含了其他所有 block。它的尺寸就是視口,即瀏覽器視窗顯示區域的尺寸。Firefox 稱之為 ViewPortFrame,而 WebKit 稱之為 RenderView。這就是文件所指向的渲染物件。渲染樹的其餘部分以 DOM 樹節點插入的形式來構建。

樣式計算

構建渲染樹時,需要計算每一個渲染物件的視覺化屬性。這是通過計算每個元素的樣式屬性來完成的。

樣式包括來自各種來源的樣式表、inline 樣式元素和 HTML 中的視覺化屬性(例如“bgcolor”屬性)。其中後者將經過轉化以匹配 CSS 樣式屬性。

樣式表的來源包括瀏覽器的預設樣式表、由網頁作者提供的樣式表以及由瀏覽器使用者提供的使用者樣式表(瀏覽器允許您定義自己喜歡的樣式。以 Firefox 為例,使用者可以將自己喜歡的樣式表放在“Firefox Profile”資料夾下)。

樣式計算存在以下難點:

  1. 樣式資料是一個超大的結構,儲存了無數的樣式屬性,這可能造成記憶體問題。
  2. 如果不進行優化,為每一個元素查詢匹配的規則會造成效能問題。要為每一個元素遍歷整個規則列表來尋找匹配規則,這是一項浩大的工程。選擇器會具有很複雜的結構,這就會導致某個匹配過程一開始看起來很可能是正確的,但最終發現其實是徒勞的,必須嘗試其他匹配路徑。

例如下面這個組合選擇器:

    div div div div{
        ...
    }
複製程式碼

這意味著規則適用於作為 3 個 div 元素的子代的

.如果您要檢查規則是否適用於某個指定的 元素,應選擇樹上的一條向上路徑進行檢查。您可能需要向上遍歷節點樹,結果發現只有兩個 div,而且規則並不適用。然後,您必須嘗試樹中的其他路徑。

  1. 應用規則涉及到相當複雜的層疊規則(用於定義這些規則的層次)。

讓我們來看看瀏覽器是如何處理這些問題的:

共享樣式資料

WebKit 節點會引用樣式物件 (Renderstyle)。這些物件在某些情況下可以由不同節點共享。這些節點是同級關係,並且:

  1. 這些元素必須處於相同的滑鼠狀態(例如,不允許其中一個是“:hover”狀態,而另一個不是)
  2. 任何元素都沒有 ID
  3. 標記名稱應匹配
  4. 類屬性應匹配
  5. 對映屬性的集合必須是完全相同的
  6. 連結狀態必須匹配
  7. 焦點狀態必須匹配
  8. 任何元素都不應受屬性選擇器的影響,這裡所說的“影響”是指在選擇器中的任何位置有任何使用了屬性選擇器的選擇器匹配
  9. 元素中不能有任何 inline 樣式屬性
  10. 不能使用任何同級選擇器。WebCore 在遇到任何同級選擇器時,只會引發一個全域性開關,並停用整個文件的樣式共享(如果存在)。這包括 + 選擇器以及 :first-child 和 :last-child 等選擇器。
Firefox規則樹

為了簡化樣式計算,Firefox 還採用了另外兩種樹:規則樹和樣式上下文樹。WebKit 也有樣式物件,但它們不是儲存在類似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類物件的相關樣式。

Firefox 樣式上下文樹

圖14: Firefox 樣式上下文樹

樣式上下文包含端值。要計算出這些值,應按照正確順序應用所有的匹配規則,並將其從邏輯值轉化為具體的值。例如,如果邏輯值是螢幕大小的百分比,則需要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間可以共享這些值,以避免重複計算,還可以節約空間。

所有匹配的規則都儲存在樹中。路徑中的底層節點擁有較高的優先順序。規則樹包含了所有已知規則匹配的路徑。規則的儲存是延遲進行的。規則樹不會在開始的時候就為所有的節點進行計算,而是隻有當某個節點樣式需要進行計算時,才會向規則樹新增計算的路徑。

這個想法相當於將規則樹路徑視為詞典中的單詞。如果我們已經計算出如下的規則樹:

詞典規則樹

圖15: 詞典規則樹

假設我們需要為內容樹中的另一個元素匹配規則,並且找到匹配路徑是 B - E - I(按照此順序)。由於我們在樹中已經計算出了路徑 A - B - E - I - L,因此就已經有了此路徑,這就減少了現在所需的工作量。 讓我們看看規則樹如何幫助我們減少工作。

結構劃分

樣式上下文可分割成多個結構。這些結構體包含了特定類別(如 border 或 color)的樣式資訊。結構中的屬性都是繼承的或非繼承的。繼承屬性如果未由元素定義,則繼承自其父代。非繼承屬性(也稱為“重置”屬性)如果未進行定義,則使用預設值。

規則樹通過快取整個結構(包含計算出的端值)為我們提供幫助。這一想法假定底層節點沒有提供結構的定義,則可使用上層節點中的快取結構。

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

在計算某個特定元素的樣式上下文時,我們首先計算規則樹中的對應路徑,或者使用現有的路徑。然後我們沿此路徑應用規則,在新的樣式上下文中填充結構。我們從路徑中擁有最高優先順序的底層節點(通常也是最特殊的選擇器)開始,並向上遍歷規則樹,直到結構填充完畢。如果該規則節點對於此結構沒有任何規範,那麼我們可以實現更好的優化:尋找路徑更上層的節點,找到後指定完整的規範並指向相關節點即可。這是最好的優化方法,因為整個結構都能共享。這可以減少端值的計算量並節約記憶體。 如果我們找到了部分定義,就會向上遍歷規則樹,直到結構填充完畢。

如果我們找不到結構的任何定義,那麼假如該結構是“繼承”型別,我們會在上下文樹中指向父代的結構,這樣也可以共享結構。如果是 reset 型別的結構,則會使用預設值。

如果最特殊的節點確實添加了值,那麼我們需要另外進行一些計算,以便將這些值轉化成實際值。然後我們將結果快取在樹節點中,供子代使用。

如果某個元素與其同級元素都指向同一個樹節點,那麼它們就可以共享整個樣式上下文。

讓我們來看一個例子,假設我們有如下 HTML 程式碼:

    <html>
    <body>
        
            
           

相關推薦

[]瀏覽器工作原理探究

引言 最近對web的效能優化比較感興趣,而前端程式碼主要在瀏覽器工作的。如果對瀏覽器的工作原理了解清楚,可以為web效能優化提供方向以及理論依據。 本文主要參考 How Browsers Work: Behind the scenes of modern web browsers 。在此基礎上整理而來的。個

瀏覽器工作原理(二):瀏覽器渲染過程概述

sync 結構 dom end 繪制 fault 異步加載 步驟 targe 參考:https://segmentfault.com/a/1190000012925872#articleHeader4 瀏覽器器內核拿到內容後,渲染大概可以劃分成以下幾個步驟: 解析html

web效能優化-瀏覽器工作原理

要徹底瞭解web效能優化的問題,得搞清楚瀏覽器的工作原理。 我們需要了解,你在瀏覽器位址列中輸入url到頁面展示的短短几秒中,瀏覽器究竟做了什麼,才能瞭解到為什麼我們口中所說的優化方案能夠起到優化作用。 瀏覽器的多程序架構(以下的例子都是以chrome為例) chrome由多個程序組成,每個程序都有自己

JS進階 - 瀏覽器工作原理

一、瀏覽器的結構 瀏覽器的主要元件為: 使用者介面 - 包括位址列、前進/後退按鈕、書籤選單等。除了瀏覽器主視窗(顯示頁面),其他部分都屬於使用者介面。 瀏覽器引擎 - 在使用者介面和渲染引擎之間傳送指令。 渲染引擎 - 顯示(渲染)請求的內容。如果請求的內容是 HTML,它就負責

瀏覽器工作原理2-域名解析

當你在位址列輸入網址https://blog.csdn.net/並按下回車的時候,瀏覽器首先需要做的就是域名解析。 我們在瀏覽器位址列輸入的通常都是域名,域名其實就是IP地址的代稱。使用域名有兩個好處: 好記。 使用這一串帶有特殊含義的字元作為域名,而不需要記憶沒有含義的數字

瀏覽器工作原理1-基本介紹

基本介紹 瀏覽器工作原理也就是當你輸入一個網址到螢幕上看到這個網站的過程中發生了什麼事情,對於前端開發人員來說,這是必須瞭解的東西1。 在介紹瀏覽器工作原理之前,先了解下當前世面上目前主流瀏覽器包括Chrome、Firefox、UC、Safari 和 Opera。根據statcoun

瀏覽器工作原理(五):CSS解析(CSS parsing)

還記得簡介中提到的解析的概念嗎,不同於html,css屬於上下文無關文法,可以用前面所描述的解析器來解析。Css規範定義了css的詞法及語法文法。 看一些例子: 每個符號都由正則表示式定義了詞法文法(詞彙表): comment///*[^*]*/*+([^/*][^*]*

深入理解瀏覽器工作原理

前面的話   瀏覽器(browser application)是專門用來訪問和瀏覽全球資訊網頁面的客戶端軟體,也是現代計算機系統中應用最為廣泛的軟體之一,其重要性不言而喻。前端工程師作為負責程式頁面顯示的工程師,需要直接與瀏覽器打交道。本文將詳細介紹瀏覽器的工作原理 組成   瀏覽器的組成如下圖所示

瀏覽器工作原理

我們在瀏覽器輸入一個url,瀏覽器無法通過一個url訪問到伺服器,只能通過IP地址才能找到web伺服器,所以瀏覽器會通過域名獲取web伺服器的IP地址即DNS解析,然後通過解析出來的IP地址去訪問伺服器,然後兩者通過HTTP協議去通訊,其完整的流程如下圖所示:

瀏覽器工作原理詳解

這篇文章是以色列開發人員塔利·加希爾的研究成果。她在查閱了所有公開發布的關於瀏覽器內部機制的資料,並花了很多時間來研讀網路瀏覽器的原始碼。她寫道: 在 IE 佔據 90%市場份額的年代,我們除了把瀏覽器當成一個“黑箱”,什麼也做不了。但是現在,開放原始碼的瀏

Spring MVC中DispatcherServlet工作原理探究

下面類圖將主要的類及方法抽離出來,以便檢視方便,根據類的結構來說明整個請求是如何工作的 主要使用到的技術有Spring的IOC容器和Servlet。 假如我們要實現一個請求home.htm然後返回home.jsp檢視資源則 當home.htm請求到達時,我們需要Disp

瀏覽器工作原理(四):HTML解析器 HTML Parser

HTML解析器的工作是將html標識解析為解析樹。 HTML文法定義(The HTML grammar definition) W3C組織制定規範定義了HTML的詞彙表和語法。 非上下文無關文法(Not a context free grammar) 正如在解析簡介中提

深入解析瀏覽器的幕後工作原理(三) 呈現樹和 DOM 樹的關系

文本 一行 出現 src 格式 關於 放置 顯示 關系 呈現樹和 DOM 樹的關系   呈現器是和 DOM 元素相對應的,但並非一一對應。非可視化的 DOM 元素不會插入呈現樹中,例如“head”元素。如果元素的 display 屬性值為“none”,那麽也不會顯示在呈現

深入解析瀏覽器的幕後工作原理(二) 呈現引擎

div 分享 image ima 好的 clas logs 指令 開放源代碼 呈現引擎   本文所討論的瀏覽器(Firefox、Chrome 瀏覽器和 Safari)是基於兩種呈現引擎構建的。Firefox 使用的是 Gecko,這是 Mozilla 公司“自制”的呈現

根據瀏覽器渲染引擎工作原理(reflow/repaint),來優化DOM的操作

工作原理 scroll 標簽 發現 較高的 所有 hid 問題 移動端 1.瀏覽器的渲染引擎工作原理: (1)解析HTML,生成DOM樹。解析HTML文檔,轉換樹中的html標簽或js生成的標簽到DOM節點,它被稱為 -- 內容樹。 (2)構建渲染樹,解析Style

】JavaScript的工作原理:引擎,執行時和呼叫堆疊的概述

原文地址:https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf(需要翻牆) 隨著javascript變得越來越流行,很多團隊的技術棧都開始使用它,比如前端、後端、hybrid、嵌入式裝置等。 這篇文

zuul原始碼分析-探究原生zuul的工作原理

前提 最近在專案中使用了SpringCloud,基於zuul搭建了一個提供加解密、鑑權等功能的閘道器服務。鑑於之前沒怎麼使用過Zuul,於是順便仔細閱讀了它的原始碼。實際上,zuul原來提供的功能是很單一的:通過一個統一的Servlet入口(ZuulServlet)攔截所有的請求,然後通過內建的com.net

】JavaScript的工作原理:記憶體管理和4種常見的記憶體洩漏

該系列的第一篇文章重點介紹了引擎,執行時和呼叫堆疊的概述。第二篇文章深入剖析了Google的V8 JavaScript引擎,並提供了關於如何編寫更好的JavaScript程式碼的一些提示。 在第三篇文章中,我們將討論另一個越來越被開發人員忽視的關鍵主題,因為日常使用的程式語言(記憶體管理)越來越成熟和複雜。

瀏覽器渲染原理及解剖瀏覽器內部工作原理

1、簡單地說,頁面渲染就是瀏覽器將html程式碼根據CSS定義的規則顯示在瀏覽器視窗中的這個過程。先來大致瞭解一下瀏覽器都是怎麼工作的:   1. 使用者輸入網址(假設是個html頁面,並且是第一次訪問),瀏覽器向伺服器發出請求,伺服器返回html檔案;   2.

】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

此篇是JavaScript的工作原理的第四篇,其它三篇可以看這裡: 【譯】JavaScript的工作原理:引擎,執行時和呼叫堆疊的概述 【譯】JavaScript的工作原理:V8引擎內部+關於如何編寫優化程式碼的5個技巧 【譯】JavaScript的工作原理:記憶體管理和4種常見的記憶體洩漏