前言
史上最長的分享bnf,編輯的時候表示情緒穩(wěn)定。今日早讀文章需要你很長的時間閱讀,由@ikeike443 和 @kiyoto01翻譯分享。
正文從這開始~
這是一篇全面介紹 WebKit 和 Gecko 內(nèi)部操作的入門文章,是以色列開發(fā)人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了所有公開發(fā)布的關(guān)于瀏覽器內(nèi)部機(jī)制的數(shù)據(jù)(請參見資源),并花了很多時間來研讀網(wǎng)絡(luò)瀏覽器的源代碼。她寫道bnf:
在 IE 占據(jù) 90% 市場份額的年代,我們除了把瀏覽器當(dāng)成一個“黑箱”,什么也做不了。但是現(xiàn)在,開放源代碼的瀏覽器擁有了過半的市場份額,因此,是時候來揭開神秘的面紗,一探網(wǎng)絡(luò)瀏覽器的內(nèi)幕了。呃,里面只有數(shù)以百萬行計的 C++ 代碼 ...
塔利在她的網(wǎng)站上公布了自己的研究成果,但是我們覺得它值得讓更多的人來了解,所以我們在此重新整理并公布。
作為一名網(wǎng)絡(luò)開發(fā)人員,學(xué)習(xí)瀏覽器的內(nèi)部工作原理將有助于您作出更明智的決策,并理解那些最佳開發(fā)實踐的個中緣由。盡管這是一篇相當(dāng)長的文檔,但是我們建議您花些時間來仔細(xì)閱讀;讀完之后,您肯定會覺得所費(fèi)不虛。
保羅·愛麗詩 (Paul Irish),Chrome 瀏覽器開發(fā)人員事務(wù)部
簡介
網(wǎng)絡(luò)瀏覽器很可能是使用最廣的軟件。在這篇入門文章中,我將會介紹它們的幕后工作原理。我們會了解到,從您在地址欄輸入 google.com 直到您在瀏覽器屏幕上看到 Google 首頁的整個過程中都發(fā)生了些什么。
我們要討論的瀏覽器
目前使用的主流瀏覽器有五個:Internet Explorer、Firefox、Safari、Chrome 瀏覽器和 Opera。本文中以開放源代碼瀏覽器為例,即 Firefox、Chrome 瀏覽器和 Safari(部分開源)。根據(jù) StatCounter 瀏覽器統(tǒng)計數(shù)據(jù),目前(2011 年 8 月)Firefox、Safari 和 Chrome 瀏覽器的總市場占有率將近 60%。由此可見,如今開放源代碼瀏覽器在瀏覽器市場中占據(jù)了非常堅實的部分。
瀏覽器的主要功能
瀏覽器的主要功能就是向服務(wù)器發(fā)出請求,在瀏覽器窗口中展示您選擇的網(wǎng)絡(luò)資源。這里所說的資源一般是指 HTML 文檔,也可以是 PDF、圖片或其他的類型。資源的位置由用戶使用 URI(統(tǒng)一資源標(biāo)示符)指定。
瀏覽器解釋并顯示 HTML 文件的方式是在 HTML 和 CSS 規(guī)范中指定的。這些規(guī)范由網(wǎng)絡(luò)標(biāo)準(zhǔn)化組織 W3C(萬維網(wǎng)聯(lián)盟)進(jìn)行維護(hù)。
多年以來,各瀏覽器都沒有完全遵從這些規(guī)范,同時還在開發(fā)自己獨有的擴(kuò)展程序,這給網(wǎng)絡(luò)開發(fā)人員帶來了嚴(yán)重的兼容性問題。如今,大多數(shù)的瀏覽器都是或多或少地遵從規(guī)范。
瀏覽器的用戶界面有很多彼此相同的元素,其中包括:
用來輸入 URI 的地址欄
前進(jìn)和后退按鈕
書簽設(shè)置選項
用于刷新和停止加載當(dāng)前文檔的刷新和停止按鈕
用于返回主頁的主頁按鈕
奇怪的是,瀏覽器的用戶界面并沒有任何正式的規(guī)范,這是多年來的最佳實踐自然發(fā)展以及彼此之間相互模仿的結(jié)果。HTML5 也沒有定義瀏覽器必須具有的用戶界面元素,但列出了一些通用的元素,例如地址欄、狀態(tài)欄和工具欄等。當(dāng)然,各瀏覽器也可以有自己獨特的功能,比如 Firefox 的下載管理器。
瀏覽器的高層結(jié)構(gòu)
瀏覽器的主要組件為 (1.1):
用戶界面 - 包括地址欄、前進(jìn)/后退按鈕、書簽菜單等。除了瀏覽器主窗口顯示的您請求的頁面外,其他顯示的各個部分都屬于用戶界面。
瀏覽器引擎 - 在用戶界面和呈現(xiàn)引擎之間傳送指令。
呈現(xiàn)引擎 - 負(fù)責(zé)顯示請求的內(nèi)容。如果請求的內(nèi)容是 HTML,它就負(fù)責(zé)解析 HTML 和 CSS 內(nèi)容,并將解析后的內(nèi)容顯示在屏幕上。
網(wǎng)絡(luò) - 用于網(wǎng)絡(luò)調(diào)用,比如 HTTP 請求。其接口與平臺無關(guān),并為所有平臺提供底層實現(xiàn)。
用戶界面后端 - 用于繪制基本的窗口小部件,比如組合框和窗口。其公開了與平臺無關(guān)的通用接口,而在底層使用操作系統(tǒng)的用戶界面方法。
Java 解釋器。用于解析和執(zhí)行 Java 代碼。
數(shù)據(jù)存儲。這是持久層。瀏覽器需要在硬盤上保存各種數(shù)據(jù),例如 Cookie。新的 HTML 規(guī)范 (HTML5) 定義了“網(wǎng)絡(luò)數(shù)據(jù)庫”,這是一個完整(但是輕便)的瀏覽器內(nèi)數(shù)據(jù)庫。
瀏覽器的主要組件
值得注意的是,和大多數(shù)瀏覽器不同,Chrome 瀏覽器的每個標(biāo)簽頁都分別對應(yīng)一個呈現(xiàn)引擎實例。每個標(biāo)簽頁都是一個獨立的進(jìn)程。
呈現(xiàn)引擎
呈現(xiàn)引擎的作用嘛...當(dāng)然就是“呈現(xiàn)”了,也就是在瀏覽器的屏幕上顯示請求的內(nèi)容。
默認(rèn)情況下,呈現(xiàn)引擎可顯示 HTML 和 XML 文檔與圖片。通過插件(或瀏覽器擴(kuò)展程序),還可以顯示其他類型的內(nèi)容;例如,使用 PDF 查看器插件就能顯示 PDF 文檔。但是在本章中,我們將集中介紹其主要用途:顯示使用 CSS 格式化的 HTML 內(nèi)容和圖片。
呈現(xiàn)引擎
本文所討論的瀏覽器(Firefox、Chrome 瀏覽器和 Safari)是基于兩種呈現(xiàn)引擎構(gòu)建的。Firefox 使用的是 Gecko,這是 Mozilla 公司“自制”的呈現(xiàn)引擎。而 Safari 和 Chrome 瀏覽器使用的都是 WebKit。
WebKit 是一種開放源代碼呈現(xiàn)引擎,起初用于 Linux 平臺,隨后由 Apple 公司進(jìn)行修改,從而支持蘋果機(jī)和 Windows。有關(guān)詳情,請參閱 webkit.org。
主流程
呈現(xiàn)引擎一開始會從網(wǎng)絡(luò)層獲取請求文檔的內(nèi)容,內(nèi)容的大小一般限制在 8000 個塊以內(nèi)。
然后進(jìn)行如下所示的基本流程:
呈現(xiàn)引擎的基本流程
呈現(xiàn)引擎將開始解析 HTML 文檔,并將各標(biāo)記逐個轉(zhuǎn)化成“內(nèi)容樹”上的 DOM 節(jié)點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數(shù)據(jù)。HTML 中這些帶有視覺指令的樣式信息將用于創(chuàng)建另一個樹結(jié)構(gòu):呈現(xiàn)樹。
呈現(xiàn)樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序。
呈現(xiàn)樹構(gòu)建完畢之后,進(jìn)入“布局”處理階段,也就是為每個節(jié)點分配一個應(yīng)出現(xiàn)在屏幕上的確切坐標(biāo)。下一個階段是繪制 - 呈現(xiàn)引擎會遍歷呈現(xiàn)樹,由用戶界面后端層將每個節(jié)點繪制出來。
需要著重指出的是,這是一個漸進(jìn)的過程。為達(dá)到更好的用戶體驗,呈現(xiàn)引擎會力求盡快將內(nèi)容顯示在屏幕上。它不必等到整個 HTML 文檔解析完畢之后,就會開始構(gòu)建呈現(xiàn)樹和設(shè)置布局。在不斷接收和處理來自網(wǎng)絡(luò)的其余內(nèi)容的同時,呈現(xiàn)引擎會將部分內(nèi)容解析并顯示出來。
主流程示例
WebKit 主流程
Mozilla 的 Gecko 呈現(xiàn)引擎主流程
從圖 3 和圖 4 可以看出,雖然 WebKit 和 Gecko 使用的術(shù)語略有不同,但整體流程是基本相同的。
Gecko 將視覺格式化元素組成的樹稱為“框架樹”。每個元素都是一個框架。WebKit 使用的術(shù)語是“呈現(xiàn)樹”,它由“呈現(xiàn)對象”組成。對于元素的放置,WebKit 使用的術(shù)語是“布局”,而 Gecko 稱之為“重排”。對于連接 DOM 節(jié)點和可視化信息從而創(chuàng)建呈現(xiàn)樹的過程,WebKit 使用的術(shù)語是“附加”。有一個細(xì)微的非語義差別,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱為“內(nèi)容槽”的層,用于生成 DOM 元素。我們會逐一論述流程中的每一部分:
解析 - 綜述
解析是呈現(xiàn)引擎中非常重要的一個環(huán)節(jié),因此我們要更深入地講解。首先,來介紹一下解析。
解析文檔是指將文檔轉(zhuǎn)化成為有意義的結(jié)構(gòu),也就是可讓代碼理解和使用的結(jié)構(gòu)。解析得到的結(jié)果通常是代表了文檔結(jié)構(gòu)的節(jié)點樹,它稱作解析樹或者語法樹。
示例 - 解析 2 + 3 - 1 這個表達(dá)式,會返回下面的樹:
數(shù)學(xué)表達(dá)式樹節(jié)點
語法
解析是以文檔所遵循的語法規(guī)則(編寫文檔所用的語言或格式)為基礎(chǔ)的。所有可以解析的格式都必須對應(yīng)確定的語法(由詞匯和語法規(guī)則構(gòu)成)。這稱為與上下文無關(guān)的語法。人類語言并不屬于這樣的語言,因此無法用常規(guī)的解析技術(shù)進(jìn)行解析。
解析器和詞法分析器的組合
解析的過程可以分成兩個子過程:詞法分析和語法分析。
詞法分析是將輸入內(nèi)容分割成大量標(biāo)記的過程。標(biāo)記是語言中的詞匯,即構(gòu)成內(nèi)容的單位。在人類語言中,它相當(dāng)于語言字典中的單詞。
語法分析是應(yīng)用語言的語法規(guī)則的過程。
解析器通常將解析工作分給以下兩個組件來處理:詞法分析器(有時也稱為標(biāo)記生成器),負(fù)責(zé)將輸入內(nèi)容分解成一個個有效標(biāo)記;而解析器負(fù)責(zé)根據(jù)語言的語法規(guī)則分析文檔的結(jié)構(gòu),從而構(gòu)建解析樹。詞法分析器知道如何將無關(guān)的字符(比如空格和換行符)分離出來。
從源文檔到解析樹
解析是一個迭代的過程。通常,解析器會向詞法分析器請求一個新標(biāo)記,并嘗試將其與某條語法規(guī)則進(jìn)行匹配。如果發(fā)現(xiàn)了匹配規(guī)則,解析器會將一個對應(yīng)于該標(biāo)記的節(jié)點添加到解析樹中,然后繼續(xù)請求下一個標(biāo)記。
如果沒有規(guī)則可以匹配,解析器就會將標(biāo)記存儲到內(nèi)部,并繼續(xù)請求標(biāo)記,直至找到可與所有內(nèi)部存儲的標(biāo)記匹配的規(guī)則。如果找不到任何匹配規(guī)則,解析器就會引發(fā)一個異常。這意味著文檔無效,包含語法錯誤。
翻譯
很多時候,解析樹還不是最終產(chǎn)品。解析通常是在翻譯過程中使用的,而翻譯是指將輸入文檔轉(zhuǎn)換成另一種格式。編譯就是這樣一個例子。編譯器可將源代碼編譯成機(jī)器代碼,具體過程是首先將源代碼解析成解析樹,然后將解析樹翻譯成機(jī)器代碼文檔。
編譯流程
解析示例
在圖 5 中,我們通過一個數(shù)學(xué)表達(dá)式建立了解析樹?,F(xiàn)在,讓我們試著定義一個簡單的數(shù)學(xué)語言,用來演示解析的過程。
詞匯:我們用的語言可包含整數(shù)、加號和減號。
語法:
構(gòu)成語言的語法單位是表達(dá)式、項和運(yùn)算符。
我們用的語言可以包含任意數(shù)量的表達(dá)式。
表達(dá)式的定義是:一個“項”接一個“運(yùn)算符”,然后再接一個“項”。
運(yùn)算符是加號或減號。
項是一個整數(shù)或一個表達(dá)式。
讓我們分析一下 2 + 3 - 1。
匹配語法規(guī)則的第一個子串是 2,而根據(jù)第 5 條語法規(guī)則,這是一個項。匹配語法規(guī)則的第二個子串是 2 + 3,而根據(jù)第 3 條規(guī)則(一個項接一個運(yùn)算符,然后再接一個項),這是一個表達(dá)式。下一個匹配項已經(jīng)到了輸入的結(jié)束。2 + 3 - 1 是一個表達(dá)式,因為我們已經(jīng)知道 2 + 3 是一個項,這樣就符合“一個項接一個運(yùn)算符,然后再接一個項”的規(guī)則。2 + + 不與任何規(guī)則匹配,因此是無效的輸入。
詞匯和語法的正式定義
詞匯通常用正則表達(dá)式表示。
例如,我們的示例語言可以定義如下:
INTEGER :0|[1-9][0-9]*
PLUS :+
MINUS:-
正如您所看到的,這里用正則表達(dá)式給出了整數(shù)的定義。
語法通常使用一種稱為 BNF 的格式來定義。我們的示例語言可以定義如下:
expression :=term operation term
operation :=PLUS |MINUS
term :=INTEGER |expression
之前我們說過,如果語言的語法是與上下文無關(guān)的語法,就可以由常規(guī)解析器進(jìn)行解析。與上下文無關(guān)的語法的直觀定義就是可以完全用 BNF 格式表達(dá)的語法。有關(guān)正式定義,請參閱關(guān)于與上下文無關(guān)的語法的維基百科文章。
解析器類型
有兩種基本類型的解析器:自上而下解析器和自下而上解析器。直觀地來說,自上而下的解析器從語法的高層結(jié)構(gòu)出發(fā),嘗試從中找到匹配的結(jié)構(gòu)。而自下而上的解析器從低層規(guī)則出發(fā),將輸入內(nèi)容逐步轉(zhuǎn)化為語法規(guī)則,直至滿足高層規(guī)則。
讓我們來看看這兩種解析器會如何解析我們的示例:
自上而下的解析器會從高層的規(guī)則開始:首先將 2 + 3 標(biāo)識為一個表達(dá)式,然后將 2 + 3 - 1 標(biāo)識為一個表達(dá)式(標(biāo)識表達(dá)式的過程涉及到匹配其他規(guī)則,但是起點是最高級別的規(guī)則)。
自下而上的解析器將掃描輸入內(nèi)容,找到匹配的規(guī)則后,將匹配的輸入內(nèi)容替換成規(guī)則。如此繼續(xù)替換,直到輸入內(nèi)容的結(jié)尾。部分匹配的表達(dá)式保存在解析器的堆棧中。
這種自下而上的解析器稱為移位歸約解析器,因為輸入在向右移位(設(shè)想有一個指針從輸入內(nèi)容的開頭移動到結(jié)尾),并且逐漸歸約到語法規(guī)則上。
自動生成解析器
有一些工具可以幫助您生成解析器,它們稱為解析器生成器。您只要向其提供您所用語言的語法(詞匯和語法規(guī)則),它就會生成相應(yīng)的解析器。創(chuàng)建解析器需要對解析有深刻理解,而人工創(chuàng)建并優(yōu)化解析器并不是一件容易的事情,所以解析器生成器是非常實用的。
WebKit 使用了兩種非常有名的解析器生成器:用于創(chuàng)建詞法分析器的 Flex 以及用于創(chuàng)建解析器的 Bison(您也可能遇到 Lex 和 Yacc 這樣的別名)。Flex 的輸入是包含標(biāo)記的正則表達(dá)式定義的文件。Bison 的輸入是采用 BNF 格式的語言語法規(guī)則。
HTML 解析器
HTML 解析器的任務(wù)是將 HTML 標(biāo)記解析成解析樹。
HTML 語法定義
HTML 的詞匯和語法在 W3C 組織創(chuàng)建的規(guī)范中進(jìn)行了定義。當(dāng)前的版本是 HTML4,HTML5 正在處理過程中。
非與上下文無關(guān)的語法
正如我們在解析過程的簡介中已經(jīng)了解到的,語法可以用 BNF 等格式進(jìn)行正式定義。
很遺憾,所有的常規(guī)解析器都不適用于 HTML(我并不是開玩笑,它們可以用于解析 CSS 和 Java)。HTML 并不能很容易地用解析器所需的與上下文無關(guān)的語法來定義。
有一種可以定義 HTML 的正規(guī)格式:DTD(Document Type Definition,文檔類型定義),但它不是與上下文無關(guān)的語法。
這初看起來很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一個 XML 變體 (XHTML),那么有什么大的區(qū)別呢?
區(qū)別在于 HTML 的處理更為“寬容”,它允許您省略某些隱式添加的標(biāo)記,有時還能省略一些起始或者結(jié)束標(biāo)記等等。和 XML 嚴(yán)格的語法不同,HTML 整體來看是一種“軟性”的語法。
顯然,這種看上去細(xì)微的差別實際上卻帶來了巨大的影響。一方面,這是 HTML 如此流行的原因:它能包容您的錯誤,簡化網(wǎng)絡(luò)開發(fā)。另一方面,這使得它很難編寫正式的語法。概括地說,HTML 無法很容易地通過常規(guī)解析器解析(因為它的語法不是與上下文無關(guān)的語法),也無法通過 XML 解析器來解析。
HTML DTD
HTML 的定義采用了 DTD 格式。此格式可用于定義 SGML 族的語言。它包括所有允許使用的元素及其屬性和層次結(jié)構(gòu)的定義。如上文所述,HTML DTD 無法構(gòu)成與上下文無關(guān)的語法。
DTD 存在一些變體。嚴(yán)格模式完全遵守 HTML 規(guī)范,而其他模式可支持以前的瀏覽器所使用的標(biāo)記。這樣做的目的是確保向下兼容一些早期版本的內(nèi)容。最新的嚴(yán)格模式 DTD 可以在這里找到:www.w3.org/TR/html4/strict.dtd
DOM
解析器的輸出“解析樹”是由 DOM 元素和屬性節(jié)點構(gòu)成的樹結(jié)構(gòu)。DOM 是文檔對象模型 (Document Object Model) 的縮寫。它是 HTML 文檔的對象表示,同時也是外部內(nèi)容(例如 Java)與 HTML 元素之間的接口。
解析樹的根節(jié)點是“Document”對象。
DOM 與標(biāo)記之間幾乎是一一對應(yīng)的關(guān)系。比如下面這段標(biāo)記:
<html>
<body>
<p>
Hello World
</p>
<div><img src="example.png"/></div>
</body>
</html>
可翻譯成如下的 DOM 樹:
示例標(biāo)記的 DOM 樹
和 HTML 一樣,DOM 也是由 W3C 組織指定的。請參見 www.w3.org/DOM/DOMTR。這是關(guān)于文檔操作的通用規(guī)范。其中一個特定模塊描述針對 HTML 的元素。HTML 的定義可以在這里找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
我所說的樹包含 DOM 節(jié)點,指的是樹是由實現(xiàn)了某個 DOM 接口的元素構(gòu)成的。瀏覽器在具體的實現(xiàn)中會有一些供內(nèi)部使用的其他屬性。
解析算法
我們在之前章節(jié)已經(jīng)說過,HTML 無法用常規(guī)的自上而下或自下而上的解析器進(jìn)行解析。
原因在于:
語言的寬容本質(zhì)。
瀏覽器歷來對一些常見的無效 HTML 用法采取包容態(tài)度。
解析過程需要不斷地反復(fù)。源內(nèi)容在解析過程中通常不會改變,但是在 HTML 中,腳本標(biāo)記如果包含 document.write,就會添加額外的標(biāo)記,這樣解析過程實際上就更改了輸入內(nèi)容。
由于不能使用常規(guī)的解析技術(shù),瀏覽器就創(chuàng)建了自定義的解析器來解析 HTML。
HTML5 規(guī)范詳細(xì)地描述了解析算法。此算法由兩個階段組成:標(biāo)記化和樹構(gòu)建。
標(biāo)記化是詞法分析過程,將輸入內(nèi)容解析成多個標(biāo)記。HTML 標(biāo)記包括起始標(biāo)記、結(jié)束標(biāo)記、屬性名稱和屬性值。
標(biāo)記生成器識別標(biāo)記,傳遞給樹構(gòu)造器,然后接受下一個字符以識別下一個標(biāo)記;如此反復(fù)直到輸入的結(jié)束。
HTML 解析流程(摘自 HTML5 規(guī)范)
標(biāo)記化算法
該算法的輸出結(jié)果是 HTML 標(biāo)記。該算法使用狀態(tài)機(jī)來表示。每一個狀態(tài)接收來自輸入信息流的一個或多個字符,并根據(jù)這些字符更新下一個狀態(tài)。當(dāng)前的標(biāo)記化狀態(tài)和樹結(jié)構(gòu)狀態(tài)會影響進(jìn)入下一狀態(tài)的決定。這意味著,即使接收的字符相同,對于下一個正確的狀態(tài)也會產(chǎn)生不同的結(jié)果,具體取決于當(dāng)前的狀態(tài)。該算法相當(dāng)復(fù)雜,無法在此詳述,所以我們通過一個簡單的示例來幫助大家理解其原理。
基本示例 - 將下面的 HTML 代碼標(biāo)記化:
<html>
<body>
Hello world
</body>
</html>
初始狀態(tài)是數(shù)據(jù)狀態(tài)。遇到字符 < 時,狀態(tài)更改為“標(biāo)記打開狀態(tài)”。接收一個 a-z 字符會創(chuàng)建“起始標(biāo)記”,狀態(tài)更改為“標(biāo)記名稱狀態(tài)”。這個狀態(tài)會一直保持到接收 > 字符。在此期間接收的每個字符都會附加到新的標(biāo)記名稱上。在本例中,我們創(chuàng)建的標(biāo)記是 html 標(biāo)記。
遇到 > 標(biāo)記時,會發(fā)送當(dāng)前的標(biāo)記,狀態(tài)改回“數(shù)據(jù)狀態(tài)”。<body> 標(biāo)記也會進(jìn)行同樣的處理。目前 html 和 body 標(biāo)記均已發(fā)出?,F(xiàn)在我們回到“數(shù)據(jù)狀態(tài)”。接收到 Hello world 中的 H 字符時,將創(chuàng)建并發(fā)送字符標(biāo)記,直到接收 </body> 中的 <。我們將為 Hello world 中的每個字符都發(fā)送一個字符標(biāo)記。
現(xiàn)在我們回到“標(biāo)記打開狀態(tài)”。接收下一個輸入字符 / 時,會創(chuàng)建 end tag token 并改為“標(biāo)記名稱狀態(tài)”。我們會再次保持這個狀態(tài),直到接收 >。然后將發(fā)送新的標(biāo)記,并回到“數(shù)據(jù)狀態(tài)”。</html> 輸入也會進(jìn)行同樣的處理。
對示例輸入進(jìn)行標(biāo)記化
樹構(gòu)建算法
在創(chuàng)建解析器的同時,也會創(chuàng)建 Document 對象。在樹構(gòu)建階段,以 Document 為根節(jié)點的 DOM 樹也會不斷進(jìn)行修改,向其中添加各種元素。標(biāo)記生成器發(fā)送的每個節(jié)點都會由樹構(gòu)建器進(jìn)行處理。規(guī)范中定義了每個標(biāo)記所對應(yīng)的 DOM 元素,這些元素會在接收到相應(yīng)的標(biāo)記時創(chuàng)建。這些元素不僅會添加到 DOM 樹中,還會添加到開放元素的堆棧中。此堆棧用于糾正嵌套錯誤和處理未關(guān)閉的標(biāo)記。其算法也可以用狀態(tài)機(jī)來描述。這些狀態(tài)稱為“插入模式”。
讓我們來看看示例輸入的樹構(gòu)建過程:
<html>
<body>
Hello world
</body>
</html>
樹構(gòu)建階段的輸入是一個來自標(biāo)記化階段的標(biāo)記序列。第一個模式是“initial mode”。接收 HTML 標(biāo)記后轉(zhuǎn)為“before html”模式,并在這個模式下重新處理此標(biāo)記。這樣會創(chuàng)建一個 HTMLHtmlElement 元素,并將其附加到 Document 根對象上。
然后狀態(tài)將改為“before head”。此時我們接收“body”標(biāo)記。即使我們的示例中沒有“head”標(biāo)記,系統(tǒng)也會隱式創(chuàng)建一個 HTMLHeadElement,并將其添加到樹中。
現(xiàn)在我們進(jìn)入了“in head”模式,然后轉(zhuǎn)入“after head”模式。系統(tǒng)對 body 標(biāo)記進(jìn)行重新處理,創(chuàng)建并插入 HTMLBodyElement,同時模式轉(zhuǎn)變?yōu)椤癷n body”。
現(xiàn)在,接收由“Hello world”字符串生成的一系列字符標(biāo)記。接收第一個字符時會創(chuàng)建并插入“Text”節(jié)點,而其他字符也將附加到該節(jié)點。
接收 body 結(jié)束標(biāo)記會觸發(fā)“after body”模式。現(xiàn)在我們將接收 HTML 結(jié)束標(biāo)記,然后進(jìn)入“after after body”模式。接收到文件結(jié)束標(biāo)記后,解析過程就此結(jié)束。
示例 HTML 的樹構(gòu)建
解析結(jié)束后的操作
在此階段,瀏覽器會將文檔標(biāo)注為交互狀態(tài),并開始解析那些處于“deferred”模式的腳本,也就是那些應(yīng)在文檔解析完成后才執(zhí)行的腳本。然后,文檔狀態(tài)將設(shè)置為“完成”,一個“加載”事件將隨之觸發(fā)。
您可以在 HTML5 規(guī)范中查看標(biāo)記化和樹構(gòu)建的完整算法
瀏覽器的容錯機(jī)制
您在瀏覽 HTML 網(wǎng)頁時從來不會看到“語法無效”的錯誤。這是因為瀏覽器會糾正任何無效內(nèi)容,然后繼續(xù)工作。
以下面的 HTML 代碼為例:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
在這里,我已經(jīng)違反了很多語法規(guī)則(“mytag”不是標(biāo)準(zhǔn)的標(biāo)記,“p”和“div”元素之間的嵌套有誤等等),但是瀏覽器仍然會正確地顯示這些內(nèi)容,并且毫無怨言。因為有大量的解析器代碼會糾正 HTML 網(wǎng)頁作者的錯誤。
不同瀏覽器的錯誤處理機(jī)制相當(dāng)一致,但令人稱奇的是,這種機(jī)制并不是 HTML 當(dāng)前規(guī)范的一部分。和書簽管理以及前進(jìn)/后退按鈕一樣,它也是瀏覽器在多年發(fā)展中的產(chǎn)物。很多網(wǎng)站都普遍存在著一些已知的無效 HTML 結(jié)構(gòu),每一種瀏覽器都會嘗試通過和其他瀏覽器一樣的方式來修復(fù)這些無效結(jié)構(gòu)。
HTML5 規(guī)范定義了一部分這樣的要求。WebKit 在 HTML 解析器類的開頭注釋中對此做了很好的概括。
解析器對標(biāo)記化輸入內(nèi)容進(jìn)行解析,以構(gòu)建文檔樹。如果文檔的格式正確,就直接進(jìn)行解析。
遺憾的是,我們不得不處理很多格式錯誤的 HTML 文檔,所以解析器必須具備一定的容錯性。
我們至少要能夠處理以下錯誤情況:
明顯不能在某些外部標(biāo)記中添加的元素。在此情況下,我們應(yīng)該關(guān)閉所有標(biāo)記,直到出現(xiàn)禁止添加的元素,然后再加入該元素。
我們不能直接添加的元素。這很可能是網(wǎng)頁作者忘記添加了其中的一些標(biāo)記(或者其中的標(biāo)記是可選的)。這些標(biāo)簽可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
向 inline 元素內(nèi)添加 block 元素。關(guān)閉所有 inline 元素,直到出現(xiàn)下一個較高級的 block 元素。
如果這樣仍然無效,可關(guān)閉所有元素,直到可以添加元素為止,或者忽略該標(biāo)記。
讓我們看一些 WebKit 容錯的示例:
使用了 </br> 而不是 <br>
有些網(wǎng)站使用了 </br> 而不是 <br>。為了與 IE 和 Firefox 兼容,WebKit 將其與 <br> 做同樣的處理。
代碼如下:
if(t->isCloseTag(brTag)&&m_document->inCompatMode()){
reportError(MalformedBRError);
t->beginTag =true;
}
請注意,錯誤處理是在內(nèi)部進(jìn)行的,用戶并不會看到這個過程。
離散表格
離散表格是指位于其他表格內(nèi)容中,但又不在任何一個單元格內(nèi)的表格。
比如以下的示例:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit 會將其層次結(jié)構(gòu)更改為兩個同級表格:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
代碼如下:
if(m_inStrayTableContent &&localName ==tableTag)
popBlock(tableTag);
WebKit 使用一個堆棧來保存當(dāng)前的元素內(nèi)容,它會從外部表格的堆棧中彈出內(nèi)部表格。現(xiàn)在,這兩個表格就變成了同級關(guān)系。
嵌套的表單元素
如果用戶在一個表單元素中又放入了另一個表單,那么第二個表單將被忽略。
代碼如下:
if(!m_currentFormElement){
m_currentFormElement =newHTMLFormElement(formTag,m_document);
}
過于復(fù)雜的標(biāo)記層次結(jié)構(gòu)
代碼的注釋已經(jīng)說得很清楚了。
示例網(wǎng)站 www.liceo.edu.mx 嵌套了約 1500 個標(biāo)記,全都來自一堆 <b> 標(biāo)記。我們只允許最多 20 層同類型標(biāo)記的嵌套,如果再嵌套更多,就會全部忽略。
bool HTMLParser::allowNestedRedundantTag(constAtomicString&tagName)
{
unsigned i =0;
for(HTMLStackElem*curr =m_blockStack;
i <cMaxRedundantTagDepth &&curr &&curr->tagName ==tagName;
curr =curr->next,i++){}
returni !=cMaxRedundantTagDepth;
}
放錯位置的 html 或者 body 結(jié)束標(biāo)記
同樣,代碼的注釋已經(jīng)說得很清楚了。
支持格式非常糟糕的 HTML 代碼。我們從不關(guān)閉 body 標(biāo)記,因為一些愚蠢的網(wǎng)頁會在實際文檔結(jié)束之前就關(guān)閉。我們通過調(diào)用 end() 來執(zhí)行關(guān)閉操作。
if(t->tagName ==htmlTag ||t->tagName ==bodyTag )
return;
所以網(wǎng)頁作者需要注意,除非您想作為反面教材出現(xiàn)在 WebKit 容錯代碼段的示例中,否則還請編寫格式正確的 HTML 代碼。
CSS 解析
還記得簡介中解析的概念嗎?和 HTML 不同,CSS 是上下文無關(guān)的語法,可以使用簡介中描述的各種解析器進(jìn)行解析。事實上,CSS 規(guī)范定義了 CSS 的詞法和語法。
讓我們來看一些示例:
詞法語法(詞匯)是針對各個標(biāo)記用正則表達(dá)式定義的:
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”是標(biāo)識符 (identifier) 的縮寫,比如類名?!皀ame”是元素的 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*]')']
;
解釋:這是一個規(guī)則集的結(jié)構(gòu):
div.error ,a.error {
color:red;
font-weight:bold;
}
div.error 和 a.error 是選擇器。大括號內(nèi)的部分包含了由此規(guī)則集應(yīng)用的規(guī)則。此結(jié)構(gòu)的正式定義是這樣的:
ruleset
:selector [','S*selector ]*
'{'S*declaration [';'S*declaration ]*'}'S*
;
這表示一個規(guī)則集就是一個選擇器,或者由逗號和空格(S 表示空格)分隔的多個(數(shù)量可選)選擇器。規(guī)則集包含了大括號,以及其中的一個或多個(數(shù)量可選)由分號分隔的聲明?!奥暶鳌焙汀斑x擇器”將由下面的 BNF 格式定義。
關(guān)于本文
譯者:@ikeike443 和 @kiyoto01
原文:https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/