總覽
瀏覽器的主要組件有
- 用戶界面 - 不解釋
- 瀏覽器引擎 - 在用戶界面和渲染引擎之間傳送指令
- 渲染引擎 - 負(fù)責(zé)顯示請(qǐng)求的內(nèi)容钾菊。例如殴泰,如果請(qǐng)求的內(nèi)容是 HTML茄袖,它就負(fù)責(zé)解析 HTML 和 CSS 內(nèi)容患朱,并將解析后的內(nèi)容顯示在屏幕上宿稀。
- 網(wǎng)絡(luò) - 用于網(wǎng)絡(luò)調(diào)用枫攀,比如 HTTP 請(qǐng)求掰茶。其接口與平臺(tái)無(wú)關(guān)顷链,并為所有平臺(tái)提供底層實(shí)現(xiàn)阎毅。
- 用戶界面后端 - 用于繪制基本的部件焚刚,比如組合框和窗口。其公開(kāi)了與平臺(tái)無(wú)關(guān)的通用接口扇调,而在底層使用操作系統(tǒng)的用戶界面方法矿咕。
- JavaScript 解釋器 - 用于解析和執(zhí)行 JavaScript 代碼。
- 數(shù)據(jù)存儲(chǔ) - 這是持久層狼钮。瀏覽器可能需要在本地保存各種數(shù)據(jù)碳柱,例如 Cookie。HTML5 中熬芜,瀏覽器也支持新的存儲(chǔ)機(jī)制,如localStorage莲镣、IndexedDB、WebSQL和文件系統(tǒng)涎拉。
需要注意的是瑞侮,和大多數(shù)瀏覽器不同的圆,Chrome 瀏覽器的每個(gè)標(biāo)簽頁(yè)都分別對(duì)應(yīng)一個(gè)渲染引擎實(shí)例。每個(gè)標(biāo)簽頁(yè)都是一個(gè)獨(dú)立的進(jìn)程半火≡铰瑁可以在任務(wù)管理器中看到有多個(gè) Chrome 進(jìn)程。
這篇文章主要介紹渲染引擎钮糖。
渲染引擎
渲染引擎一開(kāi)始會(huì)從網(wǎng)絡(luò)層獲取請(qǐng)求文檔的內(nèi)容梅掠,內(nèi)容的大小一般限制在 8kB 以?xún)?nèi)。然后進(jìn)行如下所示的基本渲染流程:
(1) 渲染引擎開(kāi)始解析 HTML 文檔藐鹤,并將各 HTML 標(biāo)記逐個(gè)轉(zhuǎn)化成“內(nèi)容樹(shù)”上的DOM 節(jié)點(diǎn)。
(2) 同時(shí)也會(huì)解析外部 CSS 文件以及元素中的樣式數(shù)據(jù)赂韵。HTML 中這些帶有視覺(jué)指令的樣式信息將用于創(chuàng)建另一個(gè)樹(shù)結(jié)構(gòu):“呈現(xiàn)樹(shù)” (RenderTree)娱节。呈現(xiàn)樹(shù)包含多個(gè)帶有視覺(jué)屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序祭示。
(3) 呈現(xiàn)樹(shù)構(gòu)建完畢之后肄满,進(jìn)入“布局”(Layout) 處理階段,也就是為每個(gè)節(jié)點(diǎn)分配一個(gè)應(yīng)出現(xiàn)在屏幕上的確切坐標(biāo)质涛。
(4) 下一個(gè)階段是"繪制"(Painting) - 渲染引擎會(huì)遍歷呈現(xiàn)樹(shù)稠歉,由用戶界面后端層將每個(gè)節(jié)點(diǎn)繪制出來(lái)。
需要著重指出的是汇陆,這是一個(gè)漸進(jìn)的過(guò)程怒炸。為達(dá)到更好的用戶體驗(yàn),呈現(xiàn)引擎會(huì)力求盡快將內(nèi)容顯示在屏幕上毡代。它不必等到整個(gè) HTML 文檔都解析完畢阅羹,就會(huì)開(kāi)始構(gòu)建呈現(xiàn)樹(shù)和設(shè)置布局。在不斷接收和處理來(lái)自網(wǎng)絡(luò)的其余內(nèi)容的同時(shí)教寂,渲染引擎會(huì)先將部分內(nèi)容解析并顯示出來(lái)捏鱼。
從上兩圖可看出,兩種內(nèi)核的瀏覽器的流程大致是一致的酪耕。
Gecko 將視覺(jué)格式化元素組成的樹(shù)稱(chēng)為“框架樹(shù)FrameTree”导梆。每個(gè)元素都是一個(gè)框架。Webkit 使用的術(shù)語(yǔ)是“呈現(xiàn)樹(shù)RenderTree”迂烁,它由“呈現(xiàn)對(duì)象”組成看尼。
對(duì)于元素的放置,Webkit 使用的術(shù)語(yǔ)是“布局Layout”盟步,而 Gecko 稱(chēng)之為“重排Reflow”狡忙。
對(duì)于連接 DOM 節(jié)點(diǎn)和可視化信息從而創(chuàng)建呈現(xiàn)樹(shù)的過(guò)程,Webkit 使用的術(shù)語(yǔ)是“附加Attachment”址芯,Gecko 稱(chēng)之為“框架構(gòu)造FrameConstructor”灾茁。
有一個(gè)細(xì)微的非語(yǔ)義差別窜觉,就是 Gecko 在 HTML 與 DOM 樹(shù)之間還有一個(gè)稱(chēng)為“內(nèi)容槽ContentSink”的層,用于生成 DOM 元素北专。
下面詳細(xì)介紹每一流程:
1. HTML Parser
1.1 解析
解析文檔是指將文檔轉(zhuǎn)化成為有意義的結(jié)構(gòu)禀挫,也就是可讓代碼理解和使用的結(jié)構(gòu)。解析得到的結(jié)果通常是代表了文檔結(jié)構(gòu)的節(jié)點(diǎn)樹(shù)拓颓,它稱(chēng)作解析樹(shù)或者語(yǔ)法樹(shù)语婴。
例如 解析 2 + 3 - 1 這個(gè)表達(dá)式,會(huì)返回下面的樹(shù)(中序遍歷):
解析是以文檔所遵循的語(yǔ)法規(guī)則為基礎(chǔ)的驶睦。所有可以解析的格式都必須對(duì)應(yīng)確定的語(yǔ)法(由詞匯和語(yǔ)法規(guī)則構(gòu)成)砰左。這稱(chēng)為上下文無(wú)關(guān)的語(yǔ)法。人類(lèi)語(yǔ)言并不屬于這樣的語(yǔ)言场航,因此無(wú)法用常規(guī)的解析技術(shù)進(jìn)行解析缠导。
解析的過(guò)程可以分成兩個(gè)子過(guò)程:詞法分析和語(yǔ)法分析。
詞法分析是將輸入內(nèi)容分割成大量標(biāo)記的過(guò)程
標(biāo)記是語(yǔ)言中的詞匯溉痢,即構(gòu)成內(nèi)容的單位僻造。在人類(lèi)語(yǔ)言中,它相當(dāng)于語(yǔ)言字典中的所有單詞孩饼。
語(yǔ)法分析是應(yīng)用語(yǔ)言的語(yǔ)法規(guī)則的過(guò)程髓削。
解析器通常將解析工作分給以下兩個(gè)組件來(lái)處理:詞法分析器(有時(shí)也稱(chēng)為標(biāo)記生成器),負(fù)責(zé)將輸入內(nèi)容分解成一個(gè)個(gè)有效標(biāo)記镀娶;而解析器負(fù)責(zé)根據(jù)語(yǔ)言的語(yǔ)法規(guī)則分析文檔的結(jié)構(gòu)立膛,從而構(gòu)建解析樹(shù)。詞法分析器知道如何將無(wú)關(guān)的字符(比如空格和換行符)分離出來(lái)梯码。
解析是一個(gè)迭代的過(guò)程旧巾。通常,解析器會(huì)向詞法分析器請(qǐng)求一個(gè)新標(biāo)記忍些,并嘗試將其與某條語(yǔ)法規(guī)則進(jìn)行匹配鲁猩。如果發(fā)現(xiàn)了匹配規(guī)則,解析器會(huì)將一個(gè)對(duì)應(yīng)于該標(biāo)記的節(jié)點(diǎn)添加到解析樹(shù)中罢坝,然后繼續(xù)請(qǐng)求下一個(gè)標(biāo)記廓握。
如果沒(méi)有規(guī)則可以匹配,解析器就會(huì)將標(biāo)記存儲(chǔ)到內(nèi)部嘁酿,并繼續(xù)請(qǐng)求標(biāo)記隙券,直至找到可與所有內(nèi)部存儲(chǔ)的標(biāo)記匹配的規(guī)則。如果找不到任何匹配規(guī)則闹司,解析器就會(huì)引發(fā)一個(gè)異常娱仔。這意味著文檔無(wú)效,包含語(yǔ)法錯(cuò)誤游桩。
1.2 翻譯
很多時(shí)候牲迫,解析樹(shù)還不是最終產(chǎn)品耐朴。解析通常是在翻譯過(guò)程中使用的,而翻譯是指將輸入文檔轉(zhuǎn)換成另一種格式盹憎。編譯就是這樣一個(gè)例子筛峭。編譯器可將源代碼編譯成機(jī)器代碼,具體過(guò)程是首先將源代碼解析成解析樹(shù)陪每,然后將解析樹(shù)翻譯成機(jī)器代碼文檔影晓。
1.3 解析示例
前面通過(guò)數(shù)學(xué)表達(dá)式建立了解析樹(shù)。現(xiàn)在檩禾,讓我們?cè)囍x一個(gè)簡(jiǎn)單的數(shù)學(xué)語(yǔ)言挂签,用來(lái)演示解析的過(guò)程。
詞匯:整數(shù)盼产、加號(hào)和減號(hào)饵婆。
語(yǔ)法:
- 構(gòu)成語(yǔ)言的語(yǔ)法單位是表達(dá)式、項(xiàng)和運(yùn)算符辆飘。
- 我們用的語(yǔ)言可以包含任意數(shù)量的表達(dá)式啦辐。
- 表達(dá)式的定義是:一個(gè)“項(xiàng)”接一個(gè)“運(yùn)算符”谓传,然后再接一個(gè)“項(xiàng)”蜈项。
- 運(yùn)算符是加號(hào)或減號(hào)。
- 項(xiàng)是一個(gè)整數(shù)或一個(gè)表達(dá)式续挟。
讓我們分析一下 2 + 3 - 1
匹配語(yǔ)法規(guī)則的第一個(gè)子串是 2 紧卒,而根據(jù)第 5 條語(yǔ)法規(guī)則,這是一個(gè)項(xiàng)诗祸。匹配語(yǔ)法規(guī)則的第二個(gè)子串是 2 + 3 跑芳,而根據(jù)第 3 條規(guī)則(一個(gè)項(xiàng)接一個(gè)運(yùn)算符,然后再接一個(gè)項(xiàng))直颅,這是一個(gè)表達(dá)式博个。下一個(gè)匹配項(xiàng)已經(jīng)到了輸入的結(jié)束。 2 + 3 - 1 是一個(gè)表達(dá)式功偿,因?yàn)槲覀円呀?jīng)知道 2 + 3 是一個(gè)項(xiàng)盆佣,這樣就符合“一個(gè)項(xiàng)接一個(gè)運(yùn)算符,然后再接一個(gè)項(xiàng)”的規(guī)則械荷。 2 + + 不與任何規(guī)則匹配共耍,因此是無(wú)效的輸入。
1.4 解析器類(lèi)型
自上而下解析器
從語(yǔ)法的高層結(jié)構(gòu)出發(fā)吨瞎,嘗試從中找到匹配的結(jié)構(gòu)
從上面那個(gè)例子來(lái)看
首先將 2 + 3 標(biāo)識(shí)為一個(gè)表達(dá)式痹兜,然后將 2 + 3 - 1 標(biāo)識(shí)為一個(gè)表達(dá)式(標(biāo)識(shí)表達(dá)式的過(guò)程涉及到匹配其他規(guī)則,但是起點(diǎn)是最高級(jí)別的規(guī)則)
自下而上解析器
自下而上的解析器將掃描輸入內(nèi)容颤诀,找到匹配的規(guī)則后字旭,將匹配的輸入內(nèi)容替換成規(guī)則对湃。如此繼續(xù)替換,直到輸入內(nèi)容的結(jié)尾谐算。部分匹配的表達(dá)式保存在解析器的堆棧中熟尉。
堆棧 | 輸入 |
---|---|
2 + 3 - 1 |
|
項(xiàng) | + 3 - 1 |
項(xiàng) 運(yùn)算 | 3 - 1 |
表達(dá)式 | - 1 |
表達(dá)式運(yùn)算符 | 1 |
表達(dá)式 |
|
這種自下而上的解析器稱(chēng)為移位歸約解析器,因?yàn)檩斎朐谙蛴乙莆唬ㄔO(shè)想有一個(gè)指針從輸入內(nèi)容的開(kāi)頭移動(dòng)到結(jié)尾)洲脂,并且逐漸歸約到語(yǔ)法規(guī)則上
1.5 HTML解析器
HTML 解析器的任務(wù)是將 HTML 標(biāo)記解析成解析樹(shù)斤儿。HTML 并不能很容易地用解析器所需的與上下文無(wú)關(guān)的語(yǔ)法來(lái)定義。
有一種可以定義 HTML 的正規(guī)格式:DTD(Document Type Definition恐锦,文檔類(lèi)型定義)往果,但它不是與上下文無(wú)關(guān)的語(yǔ)法。
概括地說(shuō)一铅,HTML 無(wú)法很容易地通過(guò)常規(guī)解析器解析(因?yàn)樗恼Z(yǔ)法不是與上下文無(wú)關(guān)的語(yǔ)法)陕贮,也無(wú)法通過(guò) XML 解析器來(lái)解析,因?yàn)?HTML 的處理比XML更為“寬容”潘飘,它允許省略某些隱式添加的標(biāo)記肮之,有時(shí)還能省略一些起始或者結(jié)束標(biāo)記等等。
具體如何解析稍后會(huì)講卜录。
1.6 DOM
解析器的輸出“解析樹(shù)”是由 DOM 元素和屬性節(jié)點(diǎn)構(gòu)成的樹(shù)結(jié)構(gòu)戈擒。DOM 是文檔對(duì)象模型 (Document Object Model) 的縮寫(xiě)。它是 HTML 文檔的對(duì)象表示艰毒,同時(shí)也是外部?jī)?nèi)容(例如 JavaScript)與 HTML 元素之間的接口筐高。
DOM 與標(biāo)記之間幾乎是一一對(duì)應(yīng)的關(guān)系。比如下面這段標(biāo)記:
<html>
<body>
<p>Hello World</p>
<div>![](example.png)</div>
</body>
</html>
可解析成如下的 DOM 樹(shù):
1.7 解析算法
我們?cè)谥罢鹿?jié)已經(jīng)說(shuō)過(guò)丑瞧,HTML 無(wú)法用常規(guī)的自上而下或自下而上的解析器進(jìn)行解析柑土。原因在于:
- 語(yǔ)言的寬容本質(zhì)
- 瀏覽器歷來(lái)對(duì)一些常見(jiàn)的無(wú)效 HTML 用法采取包容態(tài)度
- 解析過(guò)程需要不斷地反復(fù)。源內(nèi)容在解析過(guò)程中通常不會(huì)改變绊汹,但是在 HTML 中稽屏,腳本標(biāo)記如果包含 document.write,就會(huì)添加額外的標(biāo)記西乖,這樣解析過(guò)程實(shí)際上就更改了輸入內(nèi)容
HTML5規(guī)范詳細(xì)描述了解析算法狐榔。此算法由兩個(gè)階段組成:標(biāo)記化和樹(shù)構(gòu)建。
標(biāo)記化是詞法分析過(guò)程浴栽,將輸入內(nèi)容解析成多個(gè)標(biāo)記荒叼。
HTML 標(biāo)記包括起始標(biāo)記、結(jié)束標(biāo)記典鸡、屬性名稱(chēng)和屬性值被廓。
標(biāo)記生成器識(shí)別標(biāo)記,傳遞給樹(shù)構(gòu)造器萝玷,然后接受下一個(gè)字符以識(shí)別下一個(gè)標(biāo)記嫁乘;如此反復(fù)直到輸入的結(jié)束昆婿。
1.7.1 標(biāo)記化算法
該算法的輸出結(jié)果是 HTML 標(biāo)記。
該算法使用狀態(tài)機(jī)來(lái)表示蜓斧。每一個(gè)狀態(tài)接收來(lái)自輸入信息流的一個(gè)或多個(gè)字符仓蛆,并根據(jù)這些字符更新下一個(gè)狀態(tài)。當(dāng)前的標(biāo)記化狀態(tài)和樹(shù)結(jié)構(gòu)狀態(tài)會(huì)影響進(jìn)入下一狀態(tài)的決定挎春。
這意味著看疙,即使接收的字符相同,對(duì)于下一個(gè)正確的狀態(tài)也會(huì)產(chǎn)生不同的結(jié)果直奋,具體取決于當(dāng)前的狀態(tài)能庆。該算法相當(dāng)復(fù)雜,無(wú)法在此詳述脚线,所以我們通過(guò)一個(gè)簡(jiǎn)單的示例來(lái)幫助大家理解其原理搁胆。
<html>
<body>
Hello world
</body>
</html>
初始狀態(tài)是數(shù)據(jù)狀態(tài)。
遇到字符 <
時(shí)邮绿,狀態(tài)更改為“標(biāo)記打開(kāi)狀態(tài)”渠旁。接收一個(gè) a-z 字符會(huì)創(chuàng)建“起始標(biāo)記”,狀態(tài)更改為“標(biāo)記名稱(chēng)狀態(tài)”船逮。這個(gè)狀態(tài)會(huì)一直保持到接收 > 字符顾腊。在此期間接收的每個(gè)字符都會(huì)附加到新的標(biāo)記名稱(chēng)上。在本例中傻唾,我們創(chuàng)建的標(biāo)記是 html 標(biāo)記投慈。
遇到 >
標(biāo)記時(shí)承耿,會(huì)發(fā)送當(dāng)前的標(biāo)記冠骄,狀態(tài)改回“數(shù)據(jù)狀態(tài)”。 <body>
標(biāo)記也會(huì)進(jìn)行同樣的處
理加袋。目前 html
和 body
標(biāo)記均已發(fā)出×堇保現(xiàn)在我們回到“數(shù)據(jù)狀態(tài)”。接收到 Hello world 中的 H 字符時(shí)职烧,將創(chuàng)建并發(fā)送字符標(biāo)記扁誓,直到接收</body>
中的 < 。我們將為 Hello world 中的每個(gè)
字符都發(fā)送一個(gè)字符標(biāo)記蚀之。
現(xiàn)在我們回到“標(biāo)記打開(kāi)狀態(tài)”蝗敢。接收下一個(gè)輸入字符 /
時(shí),會(huì)創(chuàng)建 end tag token 并改為“標(biāo)記名稱(chēng)狀態(tài)”足删。我們會(huì)再次保持這個(gè)狀態(tài)寿谴,直到接收>
。然后將發(fā)送新的標(biāo)記失受,并回到“數(shù)據(jù)狀態(tài)”讶泰。 </html>
輸入也會(huì)進(jìn)行同樣的處理咏瑟。
1.7.2 樹(shù)構(gòu)建算法
在創(chuàng)建解析器的同時(shí),也會(huì)創(chuàng)建 Document 對(duì)象痪署。在樹(shù)構(gòu)建階段码泞,以 Document 為根節(jié)點(diǎn)的 DOM 樹(shù)也會(huì)不斷進(jìn)行修改,向其中添加各種元素狼犯。標(biāo)記生成器發(fā)送的每個(gè)節(jié)點(diǎn)都會(huì)由樹(shù)構(gòu)建器進(jìn)行處理余寥。規(guī)范中定義了每個(gè)標(biāo)記所對(duì)應(yīng)的 DOM 元素,這些元素會(huì)在接收到相應(yīng)的標(biāo)記時(shí)創(chuàng)建悯森。這些元素不僅會(huì)添加到 DOM 樹(shù)中劈狐,還會(huì)添加到開(kāi)放元素的堆棧中。此堆棧用于糾正嵌套錯(cuò)誤和處理未關(guān)閉的標(biāo)記呐馆。其算法也可以用狀態(tài)機(jī)來(lái)描述肥缔。這些狀態(tài)稱(chēng)為“插入模式”。
還是上面那個(gè)例子
<html>
<body>
Hello world
</body>
</html>
樹(shù)構(gòu)建階段的輸入是一個(gè)來(lái)自標(biāo)記化階段的標(biāo)記序列汹来。
第一個(gè)模式是“initial mode”续膳。
接收 HTML標(biāo)記后轉(zhuǎn)為“before html”模式,并在這個(gè)模式下重新處理此標(biāo)記收班。這樣會(huì)創(chuàng)建一個(gè) HTMLHtmlElement 元素坟岔,并將其附加到 Document 根對(duì)象上。
然后狀態(tài)將改為“before head”摔桦。此時(shí)我們接收“body”標(biāo)記社付。即使我們的示例中沒(méi)有“head”標(biāo)記,系統(tǒng)也會(huì)隱式創(chuàng)建一個(gè) HTMLHeadElement邻耕,并將其添加到樹(shù)中鸥咖。
現(xiàn)在我們進(jìn)入了“in head”模式,然后轉(zhuǎn)入“after head”模式兄世。系統(tǒng)對(duì) body 標(biāo)記進(jìn)行重新處理啼辣,創(chuàng)建并插入 HTMLBodyElement,同時(shí)模式轉(zhuǎn)變?yōu)椤?strong>body”御滩。
現(xiàn)在鸥拧,接收由“Hello world”字符串生成的一系列字符標(biāo)記。接收第一個(gè)字符時(shí)會(huì)創(chuàng)建并插入“Text”節(jié)點(diǎn)削解,而其他字符也將附加到該節(jié)點(diǎn)富弦。
接收 body 結(jié)束標(biāo)記會(huì)觸發(fā)“after body”模式。現(xiàn)在我們將接收 HTML 結(jié)束標(biāo)記氛驮,然后進(jìn)入“after after body”模式腕柜。接收到文件結(jié)束標(biāo)記后,解析過(guò)程就此結(jié)束。
1.8 解析結(jié)束后的操作
在此階段媳握,瀏覽器會(huì)將文檔標(biāo)注為交互狀態(tài)碱屁,并開(kāi)始解析那些處于“deferred”模式的腳本,也就是那些應(yīng)在文檔解析完成后才執(zhí)行的腳本蛾找。然后娩脾,文檔狀態(tài)將設(shè)置為“完成”,一個(gè)“加載”事件將隨之觸發(fā)打毛。
可在HTML5規(guī)范中查看完整算法
1.9 瀏覽器的容錯(cuò)機(jī)制
原文鏈接:
http://bubkoo.com/2014/01/06/how-browsers-work-behind-the-scenes-of-modern-web-browsers/