瀏覽器是怎樣工做的:渲染引擎,HTML解析

渲染引擎

渲染引擎的職責是……渲染,也就是把請求的內容顯示到瀏覽器屏幕上。html

默認狀況下渲染引擎能夠顯示HTML,XML文檔以及圖片。 經過插件(瀏覽器擴展)它能夠顯示其它類型文檔。好比使用PDF viewer插件顯示PDF文件。咱們會在一個專門的章節討論插件與擴展。在這一節咱們將專一渲染引擎的主要用途——顯示用CSS格式化的HTML與圖片。html5

各類渲染引擎

咱們提到的Firefox, Safari兩種瀏覽器構建於兩種渲染引擎之上:Firefox使用Gecko —— Mozilla自家的渲染引擎;Safari 和 Chrome 都使用 Webkit。web

Webkit 是一個開源的渲染引擎,它源自Linux平臺上的一個引擎,通過Apple公司的修改能夠支持Mac與Windows平臺。更多信息能夠參考: http://webkit.org/ 。正則表達式

主要流程

渲染引擎開始於從網絡層獲取請求內容,通常是不超過8K的數據塊。接下來就是渲染引擎的基本工做流程:算法

 
圖 2:渲染引擎的基本工做流程(解析HTML構建DOM樹,渲染樹構建,渲染樹佈局,繪製渲染樹)。

渲染引擎會解析HTML文檔並把標籤轉換成內容樹中的DOM節點。它會解析style元素和外部文件中的樣式數據。樣式數據和HTML中的顯示控制將共同用來建立另外一棵樹——渲染樹express

渲染樹包含帶有顏色,尺寸等顯示屬性的矩形。這些矩形的順序與顯示順序一致。後端

渲染樹構建完成後就是」佈局「處理,也就是肯定每一個節點在屏幕上的確切顯示位置。 下一個步驟是 繪製 —— 遍歷渲染樹並用UI後端層將每個節點繪製出來。瀏覽器

必定要理解這是一個緩慢的過程,爲了更好的用戶體驗,渲染引擎會嘗試儘快的把內容顯示出來。它不會等到全部HTML都被解析完才建立並佈局渲染樹。它會 在處理後續內容的同時把處理過的局部內容先展現出來。網絡

主要流程示例

 


圖 3:Webkit主要流程
圖 4:Mozilla的Gecko渲染引擎主要流程( 3.6)

從圖3和圖4中能夠看出,儘管Webkit與Gecko使用略微不一樣的術語,這個過程仍是基本相同的。
Gecko 裏把格式化好的可視元素稱作「幀樹」(Frame tree)。每一個元素就是一個幀(frame)。 Webkit 則使用」渲染樹」這個術語,渲染樹由」渲染對象」組成。Webkit 裏使用」layout」表示元素的佈局,Gecko則稱爲」Reflow」。Webkit使用」Attachment」來鏈接DOM節點與可視化信息以構建渲染樹。一個非語義上的小差異是Gecko在HTML與DOM樹之間有一個附加的層 ,稱做」content sink」,是建立DOM對象的工廠。咱們會討論流程中的每一部分。工具

 

解析

由於解析是渲染引擎中一個很重要的處理,咱們會講的略深刻一些。讓咱們從一個小的解析介紹開始。

解析一個文檔意味着把它翻譯成有意義的結構以供代碼使用。解析的結果一般是一個表徵文檔的由節點組成的樹,稱爲解析樹或句法樹。

示例——解析表達式」2 + 3 – 1″能夠返回下面的樹:


圖 5:數學表達式樹節點

語法

解析是基於文檔所遵循的語法規則——書寫所用的語言或格式——來進行的。每一種能夠解析的格式必須由肯定的語法與詞彙組成。這被稱之爲上下文無關語法。 人類語言並不是此種語言,因此不能用常規的解析技術來解析。

解析器——詞法分析器組合

解析器有兩個處理過程——詞法分析與句法分析。

詞法分析負責把輸入切分紅符號序列,符號是語言的詞彙——由該語言全部合法的單詞組成。

句法分析是對該語言句法法則的應用。

解析器一般把工做分給兩個組件——分詞程序負責把輸入切分紅合法符號序列,解析程序負責按照句法規則分析文檔結構和構建句法樹。詞法分析器知道如何過濾像空格,換行之類的無關字符。


圖 6:從源文檔到解析樹(文檔,詞法分析,句法分析,解析樹)。

解析過程是交互式的。解析器一般會從詞法分析器獲取新符號並嘗試匹配句法規則。若是匹配成功,就在句法樹上建立相應的節點,並繼續從詞法分析器獲取下一個符號。若是沒有匹配的規則,解析器會內部保存這個符號,並繼續從詞法分析器獲取符號,直到內部保存的全部符號可以成功匹配一個規則。若是最終沒法匹配,解析器會拋出異常。這意味着文檔無效,含有句法錯誤。

轉換

多數狀況下解析樹並不是最終結果。解析常常是爲了從輸入文檔轉換成另一種格式。好比編譯器要把源碼編譯成機器碼,會首先解析成解析樹,再把解析樹轉換成機器碼。


圖 7:編譯過程(源碼,解析,解析樹,轉換,機器碼)。

解析示例

在圖5中咱們構建了一個數學表達式解析樹。讓咱們來試着定義一個簡單的數學語言並看看解析是如何進行的。

詞彙:咱們的語言能夠包含整數,加號和減號。

句法:

  1. 句法塊由表達式,術語及操做符組成。
  2. 咱們的語言能夠包含任意數量表達式。
  3. 表達式定義爲術語緊跟着操做符,再跟另一個術語。
  4. 操做符是加號或減號。
  5. 術語能夠是整數或表達式。

讓咱們分析輸入」2 + 3 – 1″。

第一個符合規則的子字符串是」2″,根據規則#5它是一個術語。第二個匹配是」2 + 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完整的表達。其規範定義請參考 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  

這種自下而上的解析器叫做移位歸約解析器,由於輸入被向右移動(想象一下一個指針從指向輸入開始逐漸向右移動) 並逐漸歸約到句法樹。

自動建立解析器

有一些工具能夠爲你建立解析器,它們一般稱爲解析器生成器。你只須要提供語法——詞彙與句法規則——它就能生成一個能夠工做的解析器。建立解析器須要對解析器有深刻的瞭解,而且手動建立一個優化的解析器並不容易,因此解析器生成工具頗有用。

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解析並不簡單,現成的上下文相關解析器搞不定,XML解析器也不行。

HTML DTD

HTML的定義使用DTD文件。這種格式用來定義SGML族語言,它包含對全部容許的元素的定義,包括它們的屬性和層級關係。如咱們前面所說,HTML DTD構不成上下文無關語法。

DTD有幾種不一樣類型。嚴格模式徹底尊守規範,但其它模式爲了向前兼容可能包含對早期瀏覽器所用標籤的支持。當前的嚴格模式DTD:http://www.w3.org/TR/html4/strict.dtd

DOM

解析器輸出的樹是由DOM元素和屬性節點組成的。DOM的全稱爲:Document Object Model。它是HTML文檔的對象化描述,也是HTML元素與外界(如Javascript)的接口。

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特有元素: 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中符號就是開始標籤,結束標籤,屬性名稱和屬生值。

分詞器識別這些符號並將其送入樹構建者,而後繼續分析處理下一個符號,直到輸入結束。


圖 6: HTML解析流程 (源自HTML5規範)

 

分詞算法

算法的輸出是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」狀態。吃掉」H」(」Hello world」第一個字母)時會產生一個字符符號,直到碰到」</body>」的」<「符號,咱們就完成了一個字符符號」Hello world」。

如今咱們回到「Tag open state」狀態。吃掉下一個輸入」/」時會產生一個」end tag token」並變動爲「Tag name state」狀態。一樣,此狀態保持到咱們碰到」>」時。這時新標籤符號完成,咱們又回到「Data state」。一樣」</html>」也會被這樣處理。


圖 9: 示例輸入源的分詞處理

 

樹的構建算法

當解析器被建立時,文檔對象也被建立了。在樹的構建過程當中DOM樹的根節點(Documen)將被修改,元素被添加到上面去。每一個分詞器完成的節點都會被樹構建器處理。規範中定義了每個符號與哪一個DOM對象相關。除了把元素添加到DOM樹外,它還會被添加到一個開放元素堆棧。這個堆棧用於糾正嵌套錯誤和標籤未關閉錯誤。這個算法也用狀態機描述,它的狀態叫作」insertion modes」。

讓咱們看看下面輸入的樹構建過程:

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

樹的構建過程當中,輸入就是分詞過程當中獲得的符號序列。第一個模式叫「initial mode」。接收 html 符號後會變成「before html」模式並從新處理此模式中的符號。這會建立一個HTMLHtmlElement元素並追加到根文檔節點。

而後狀態改變爲「before head」。咱們收到」body」時,會隱式建立一個HTMLHeadElement,儘管咱們沒有這個標籤,它也會被建立並添加到樹中。

如今咱們進入「in head」模式,而後是「after head」,Body會被從新處理,建立HTMLBodyElement元素並插入,而後進入「in body」模式。

字符符號」Hello world」收到後會建立一個」Text」節點,全部字符都被一一追加到上面。

收到body結束標籤後進入 「after body」 模式,收到html結束標籤後進入「after after body」模式。全部符號處理完後將終止解析。


圖 10: 示例HTML樹的構建

解析結束後的動做

在這一階段瀏覽器會把文檔標記爲交互模式,並開始解析deferred模式的script。」deferred」意味着腳本應該在文檔解析完成後執行。腳本處理完成後將進入」complete」狀態,」load」事件發生。

HTML5規範中包含了完整的算法: http://www.w3.org/TR/html5/syntax.html#html-parser

瀏覽器的容錯

你永遠不會看到HTML頁面語法錯誤。瀏覽器會修正錯誤並繼續。看看下面的例子:

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

我必定違背了幾百萬條規則(」my tag」是非法標籤,」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容錯的例子:

使用</br>代替<br>

有些站點使用</br>而不是<br>。爲了更好的與IE和Firefox兼容,Webkit將其視爲<br>。代碼以下:

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是一個層級過深的典型,它用大量的<b>嵌套到1500個標籤的深度。咱們只容許同一標籤連續出現20次,超過的話,全部此標籤都會被忽略。
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結束標籤位置

註釋仍然很明瞭:

支持真正的錯誤html
咱們永遠不關閉tag,由於有些愚蠢的網頁在文檔真正結束以前就關閉了它。
讓咱們用end()來關閉標籤。
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

因此網頁做者們當心了,除非你想寫一個Webkit容錯的示例代碼,不然請按正確格式書寫HTML。

 

本文做者:hfliu 轉載請註明來自:攜程設計委員會

 

英文文檔:

http://taligarsiel.com/Projects/howbrowserswork1.htm

相關文章
相關標籤/搜索