1. 介紹
瀏覽器可能是最廣泛使用的軟件。本書(shū)將介紹瀏覽器的工作原理。我們將看到福稳,當(dāng)你在地址欄中輸入google.com
直到你看到Google頁(yè)面残拐,這個(gè)過(guò)程都發(fā)生了什么途茫。
1.1 本文將討論的瀏覽器
現(xiàn)在有五種主流瀏覽器——Internet Explorer,F(xiàn)irefox溪食,Safari囊卜,Chrome和Opera。本書(shū)會(huì)基于開(kāi)源瀏覽器的例子——Firefox,Chrome以及Safari栅组,Safari是部分開(kāi)源的雀瓢。
根據(jù)W3C Browser Statistics的統(tǒng)計(jì)數(shù)據(jù),當(dāng)前(2009年10月)Firefox玉掸,Safari和Chrome的總市場(chǎng)占有率接近60%刃麸。因此,可以說(shuō)開(kāi)源瀏覽器已經(jīng)占據(jù)了瀏覽器市場(chǎng)的半壁江山司浪。(譯注:截至到2016年八月泊业,Chrome占58.1%、Safari占12.7%以及Firefox占12.4%啊易,三者總市場(chǎng)占有83.2%)
1.2 瀏覽器的主要功能
瀏覽器的主要功能是將你選擇的Web資源呈現(xiàn)出來(lái)吁伺,通過(guò)從服務(wù)器請(qǐng)求資源,然后將它在瀏覽器窗口中顯示租谈。資源的格式通常是HTML躯枢,但也包括PDF适掰、圖片以及其他格式饿凛。用戶通過(guò)URI(Uniform Resource Identifier遇汞,統(tǒng)一資源標(biāo)識(shí)符)來(lái)定位資源,我們會(huì)在網(wǎng)絡(luò)這章詳細(xì)介紹呻逆。
HTML和CSS規(guī)范中規(guī)定了瀏覽器解釋和呈現(xiàn)HTML文檔的方式夸赫。這些規(guī)范由W3C(World Wide Web Consortium)組織進(jìn)行維護(hù),它是負(fù)責(zé)制定Web標(biāo)準(zhǔn)的組織页慷。
HTML規(guī)范的當(dāng)前版本是HTML4憔足,HTML5還在指定中。CSS規(guī)范的當(dāng)前版本是CSS2酒繁,CSS3也還在指定中滓彰。(譯注:本文寫(xiě)作時(shí)間是2009年)
過(guò)去這些年瀏覽器廠商紛紛開(kāi)發(fā)自己的擴(kuò)展,只遵循一部分規(guī)范州袒,這給Web開(kāi)發(fā)者造成了嚴(yán)重的兼容性問(wèn)題〗野螅現(xiàn)如今,大多數(shù)的瀏覽器或多或少遵循規(guī)范郎哭。
但是瀏覽器的用戶界面有很多相同點(diǎn)他匪,常見(jiàn)的用戶界面元素包括:
- 地址欄,用于輸入U(xiǎn)RI
- 前進(jìn)按鈕和后退按鈕
- 書(shū)簽選項(xiàng)
- 刷新按鈕和停止按鈕夸研,用于刷新和停止加載當(dāng)前文檔
- 主頁(yè)按鈕邦蜜,幫助你直達(dá)主頁(yè)
奇怪的是,瀏覽器的用戶界面并沒(méi)有在任何正式的規(guī)范中指定亥至,它只是各瀏覽器廠商多年的經(jīng)驗(yàn)和相互模仿不斷改進(jìn)的結(jié)果悼沈。HTML5規(guī)范沒(méi)有規(guī)定瀏覽器必須具有的UI元素贱迟,但是列出了一些常用的元素,包括地址欄絮供、狀態(tài)欄以及工具欄衣吠。很顯然,有些瀏覽器有自己特有的功能壤靶,如:Firefox的下載管理器缚俏。在用戶界面這一章我們會(huì)詳細(xì)介紹。
1.3 瀏覽器的主要構(gòu)成
瀏覽器的主要組件包括:
- 用戶界面——包括地址欄贮乳、后退/前進(jìn)按鈕忧换、書(shū)簽菜單等,也就是你看到的除了用來(lái)顯式你請(qǐng)求頁(yè)面的主窗口之外的其余部分向拆。
- 瀏覽器引擎——查詢和操作渲染引擎的接口包雀。
- 渲染引擎——用來(lái)顯示請(qǐng)求的內(nèi)容。例如:如果請(qǐng)求內(nèi)容為HTML亲铡,它負(fù)責(zé)解析HTML和CSS,并將解析后的結(jié)果在屏幕上顯示葡兑。
- 網(wǎng)絡(luò)——用來(lái)完成網(wǎng)絡(luò)調(diào)用奖蔓,比如HTTP請(qǐng)求。它具有平臺(tái)無(wú)關(guān)的接口讹堤,在不同平臺(tái)實(shí)現(xiàn)不同吆鹤。
- UI后端——用來(lái)繪制基本組件,例如:組合下拉框和窗口等洲守。具有平臺(tái)無(wú)關(guān)的通用接口疑务,底層使用操作系統(tǒng)的用戶接口實(shí)現(xiàn)。
- JavaScript解釋器——用來(lái)解釋和執(zhí)行JavaScript代碼梗醇。
-
數(shù)據(jù)存儲(chǔ)——屬于持久層知允。瀏覽器需要在硬盤(pán)上保存各種各樣的數(shù)據(jù),例如Cookies叙谨。HTML5規(guī)范中定義了
Web Database
技術(shù)温鸽,這是一種完整(且輕量)的瀏覽器端數(shù)據(jù)庫(kù)。
值得注意的是手负,不同于大多數(shù)的瀏覽器涤垫,Chrome為每個(gè)Tab分配了一個(gè)單獨(dú)的渲染引擎實(shí)例,每個(gè)Tab都是一個(gè)獨(dú)立的進(jìn)程竟终。我會(huì)為每個(gè)組件獨(dú)立一章蝠猬,與你們?cè)敿?xì)討論。
1.4 組件間通信
Firefox和Chrome都開(kāi)發(fā)了一個(gè)特殊的通信基礎(chǔ)設(shè)施统捶,它們將在一個(gè)專門(mén)的章節(jié)中討論榆芦。
2. 渲染引擎
渲染引擎的職責(zé)就是渲染柄粹,也就是在瀏覽器屏幕上顯示請(qǐng)求的內(nèi)容。
默認(rèn)情況下歧杏,渲染引擎可以顯示HTML镰惦、XML文檔和圖片。它可以借助插件(一種瀏覽器擴(kuò)展)顯示其他類型的數(shù)據(jù)犬绒。例如旺入,使用PDF閱讀器插件顯示PDF文檔。有專門(mén)的一章講解插件及擴(kuò)展凯力,本章只關(guān)注渲染引擎的主要用途——顯示CSS格式化之后的HTML和圖片茵瘾。
2.1 渲染引擎
我們所討論的瀏覽器——Firefox、Chrome和Safari是基于兩種渲染引擎構(gòu)建的咐鹤。Firefox使用Gecko——Mozilla自主研發(fā)的渲染引擎拗秘。Safari和Chrome都使用Webkit。
Webkit是一款開(kāi)源的渲染引擎祈惶,它本來(lái)是為L(zhǎng)inux平臺(tái)研發(fā)的雕旨,后來(lái)被Apple修改移植到了Mac和Windows上。更多細(xì)節(jié)請(qǐng)參考https://webkit.org/捧请。
2.2 主流程
渲染引擎首先通過(guò)網(wǎng)絡(luò)層獲取請(qǐng)求文檔的內(nèi)容凡涩,通常以8K分塊的方式完成。取得內(nèi)容之后疹蛉,渲染引擎的基本流程如下:解析HTML以構(gòu)建DOM樹(shù) -> 構(gòu)建Render樹(shù) -> 布局Render樹(shù) -> 繪制Render樹(shù)活箕。
渲染引擎開(kāi)始解析HTML文檔,并將標(biāo)簽轉(zhuǎn)為內(nèi)容樹(shù)中的DOM節(jié)點(diǎn)可款。接下來(lái)育韩,它解析外部CSS文件和style
標(biāo)簽中的樣式信息。這些樣式信息和HTML中的可見(jiàn)指令將被用來(lái)構(gòu)建另一棵樹(shù)——Render樹(shù)(渲染樹(shù))闺鲸。
Render樹(shù)由一些包含視覺(jué)屬性(如顏色和大薪钐帧)的矩形組成,它們將按照正確的順序顯示到屏幕上翠拣。
Render樹(shù)構(gòu)建好之后將會(huì)執(zhí)行布局過(guò)程版仔,這意味著它將確定每個(gè)節(jié)點(diǎn)在屏幕上的確切坐標(biāo)。下一步就是繪制——遍歷Render樹(shù)并使用UI后端層繪制每個(gè)節(jié)點(diǎn)误墓。
值得注意的是蛮粮,這個(gè)過(guò)程是逐步完成的。為了更好的用戶體驗(yàn)谜慌,渲染引擎會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上然想,并不會(huì)等到所有的HTML都解析完成之后再去構(gòu)建和布局Render樹(shù)。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容欣范,同時(shí)從網(wǎng)絡(luò)上下載剩余內(nèi)容变泄。
值得注意的是令哟,這個(gè)過(guò)程是逐步完成的,為了更好的用戶體驗(yàn)妨蛹,渲染引擎將會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上屏富,并不會(huì)等到所有的html都解析完成之后再去構(gòu)建和布局render樹(shù)。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容蛙卤,同時(shí)進(jìn)程還在從網(wǎng)絡(luò)上下載其余內(nèi)容狠半。
2.3 主流程案例
從圖3和圖4中可以看出,盡管Webkit和Gecko使用的術(shù)語(yǔ)稍有不同颤难,但主流程基本相同神年。
Gecko稱可見(jiàn)的格式化元素組成的樹(shù)為Frame Tree,每個(gè)元素都是一個(gè)Frame行嗤;而Webkit使用術(shù)語(yǔ)Render Tree來(lái)表示由Render Object組成的樹(shù)已日。Webkit使用術(shù)語(yǔ)Layout表示元素的定位,而Gecko中稱為Reflow栅屏。Webkit使用術(shù)語(yǔ)Attachment表示連接DOM節(jié)點(diǎn)和樣式信息去構(gòu)建Render樹(shù)的過(guò)程飘千。這里有個(gè)微小的非語(yǔ)義上的不同,Gecko在HTML和DOM樹(shù)之間附加了一層栈雳,它被稱為Content Sink占婉,是制造DOM元素的工廠。下面將討論流程中的各個(gè)階段甫恩。
2.4 解析與構(gòu)建DOM樹(shù)
2.4.1 解析概述
既然解析是渲染引擎中一個(gè)非常重要的過(guò)程,我們將稍微深入地研究它酌予。首先簡(jiǎn)要介紹下解析磺箕。
解析一個(gè)文檔就是將其轉(zhuǎn)換為具有一定意義的結(jié)構(gòu)——某些代碼能夠理解和使用的東西。解析的結(jié)果通常是表示文檔結(jié)構(gòu)的節(jié)點(diǎn)樹(shù)抛虫,它被稱為解析樹(shù)或語(yǔ)法樹(shù)松靡。
例如:解析2 + 3 - 1
這個(gè)表達(dá)式可能返回這樣一棵樹(shù):
2.4.1.1 文法
解析基于文檔遵循的語(yǔ)法規(guī)則——寫(xiě)入文檔的語(yǔ)言或格式。每種能被解析的格式建椰,必須具有詞匯以及語(yǔ)法規(guī)則組成的特定的文法雕欺,稱為上下文無(wú)關(guān)文法。人類語(yǔ)言不具有這種特性棉姐,因此不能被傳統(tǒng)的解析技術(shù)所解析屠列。
2.4.1.2 解析器與詞法分析器
解析可以分為兩個(gè)子過(guò)程——詞法分析和語(yǔ)法分析。
詞法分析就是將輸入分解為符號(hào)伞矩,符號(hào)就是語(yǔ)言的詞匯表——有效構(gòu)建塊的集合笛洛。在人類語(yǔ)言中,相當(dāng)于這門(mén)語(yǔ)言字典中出現(xiàn)的所有單詞乃坤。
語(yǔ)法分析是指對(duì)語(yǔ)言應(yīng)用語(yǔ)法規(guī)則苛让。
解析器通常將工作分配給兩個(gè)組件——詞法分析器(有時(shí)也叫分詞器)負(fù)責(zé)將輸入分解為合法的符號(hào)沟蔑,解析器則根據(jù)語(yǔ)言的語(yǔ)法規(guī)則分析文檔結(jié)構(gòu),從而構(gòu)建解析樹(shù)狱杰。詞法分析器知道如何去掉無(wú)關(guān)字符(如空格和換行符)瘦材。
解析過(guò)程是迭代的。解析器總是會(huì)從詞法分析器那取一個(gè)新的符號(hào)仿畸,并試著用這個(gè)符號(hào)匹配一條語(yǔ)法規(guī)則食棕。如果匹配了一條規(guī)則,這個(gè)符號(hào)對(duì)應(yīng)的節(jié)點(diǎn)將被添加到解析樹(shù)上颁湖,然后解析器會(huì)繼續(xù)請(qǐng)求下一個(gè)符號(hào)宣蠕。如果沒(méi)有匹配到規(guī)則,解析器會(huì)在內(nèi)部保存該符號(hào)甥捺,并從詞法分析器取下一個(gè)符號(hào)抢蚀,直到所有內(nèi)部保存的符號(hào)能夠匹配一條語(yǔ)法規(guī)則。如果最終沒(méi)有找到匹配的規(guī)則镰禾,解析器將拋出一個(gè)異常皿曲,這意味著文檔是無(wú)效的或者包含語(yǔ)法錯(cuò)誤。
2.4.1.3 轉(zhuǎn)換
很多時(shí)候解析樹(shù)并不是最終產(chǎn)品吴侦。解析一般在轉(zhuǎn)換中使用——將輸入文檔轉(zhuǎn)換成另一種格式屋休。編譯就是個(gè)例子,編譯器將源代碼編譯成機(jī)器碼的時(shí)候备韧,先將源代碼解析為解析樹(shù)劫樟,然后將該樹(shù)轉(zhuǎn)換成機(jī)器碼文檔。
2.4.1.4 解析實(shí)例
在圖5中织堂,我們從一個(gè)數(shù)學(xué)表達(dá)式構(gòu)建了一棵解析樹(shù)叠艳。我們?cè)谶@里定義一個(gè)簡(jiǎn)單的數(shù)學(xué)語(yǔ)言來(lái)分析下解析過(guò)程。
詞匯表:我們的語(yǔ)言包括整數(shù)易阳、加號(hào)以及減號(hào)附较。
語(yǔ)法:
- 該語(yǔ)言的語(yǔ)法基本單元包括表達(dá)式、terms以及操作符潦俺。
- 該語(yǔ)言可以包括多個(gè)表達(dá)式拒课。
- 一個(gè)表達(dá)式定義為兩個(gè)term通過(guò)一個(gè)操作符連接。
- 操作符可以是加號(hào)或減號(hào)事示。
- 一個(gè)term可以是一個(gè)整數(shù)或一個(gè)表達(dá)式早像。
現(xiàn)在來(lái)分析下2 + 3 - 1
這個(gè)輸入。第一個(gè)匹配規(guī)則的子串是2
肖爵,根據(jù)規(guī)則5扎酷,它是一個(gè)term。第二個(gè)匹配的是2 + 3
遏匆,它符合規(guī)則3——一個(gè)表達(dá)式定義為兩個(gè)term通過(guò)一個(gè)操作符連接法挨。下一次匹配發(fā)生在輸入的結(jié)尾處谁榜。2 + 3 - 1
是一個(gè)表達(dá)式,因?yàn)槲覀円呀?jīng)知道2 + 3
是一個(gè)term凡纳,所以我們有了一個(gè)term緊跟著一個(gè)操作符再緊跟著另一個(gè)term窃植。2 + +
不會(huì)匹配任何規(guī)則,因此是一個(gè)無(wú)效輸入荐糜。
2.4.1.5 詞匯和語(yǔ)法的形式定義
詞匯表通常用正則表達(dá)式來(lái)定義巷怜。例如上面的語(yǔ)言可以定義為:
INTEGER: 0|[1-9][1-9]*
PLUS: +
MINUS: -
如你所見(jiàn),這里用正則表達(dá)式定義整數(shù)暴氏。
語(yǔ)法通常用BNF來(lái)定義延塑,上面的語(yǔ)言可以定義為:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
我們上面提到過(guò),如果一個(gè)語(yǔ)言的文法是上下文無(wú)關(guān)的答渔,則可以用正則解析器來(lái)解析关带。對(duì)上下文無(wú)關(guān)的文法的一個(gè)直觀定義是,該文法可以用BNF完整的表達(dá)沼撕。正式定義請(qǐng)參考維基百科詞條Context-free grammar宋雏。
2.4.1.6 解析器類型
有兩種基本的解析器——自頂向下的解析器和自底向上的解析器。一個(gè)比較直觀的解釋是:自頂向下解析器查看語(yǔ)法的最高層結(jié)構(gòu)务豺,并試圖匹配其中一個(gè)磨总;自底向上解析器則從輸入開(kāi)始,逐步將其轉(zhuǎn)換為語(yǔ)法規(guī)則笼沥,從底層規(guī)則開(kāi)始直到匹配高層規(guī)則蚪燕。
來(lái)看一下這兩種解析器如何解析上面的例子:
自頂向下解析器從最高層規(guī)則開(kāi)始,它會(huì)先識(shí)別出2 + 3
奔浅,將其視為一個(gè)表達(dá)式邻薯;然后識(shí)別出2 + 3 - 1
是一個(gè)表達(dá)式(識(shí)別表達(dá)式的過(guò)程中匹配了其他規(guī)則,但是起點(diǎn)是最高層規(guī)則)乘凸。
自底向上解析器會(huì)掃描輸入,直到匹配了一條規(guī)則累榜,然后用該規(guī)則替換匹配的輸入营勤,直到解析完所有輸入。部分匹配的表達(dá)式被放置在解析堆棧中壹罚。
Stack | Input |
---|---|
2 + 3 - 1 | |
term | + 3 - 1 |
term operation | 3 - 1 |
expression | - 1 |
expression operation | 1 |
expression |
2.4.1.7 自動(dòng)生成解析器
有工具可以自動(dòng)生成解析器葛作,它們被稱為解析器生成器。你只需要指定語(yǔ)言的文法——詞匯表和語(yǔ)法規(guī)則猖凛,它就可以生成一個(gè)解析器赂蠢。創(chuàng)建一個(gè)解析器需要對(duì)解析有深入的理解,而且手動(dòng)創(chuàng)建一個(gè)較好性能的解析器并不容易辨泳,所以解析器生成器非常有用虱岂。
Webkit使用兩個(gè)著名的解析器生成器——用于創(chuàng)建詞法分析器的Flex和創(chuàng)建解析器的Bison(你可能接觸過(guò)Lex和Yacc)玖院。Flex的輸入是一個(gè)包含符號(hào)定義的正則表達(dá)式文件,Bison的輸入是用BNF格式定義的語(yǔ)法規(guī)則第岖。
2.4.2 HTML解析器
HTML解析器的工作是將HTML標(biāo)簽解析為解析樹(shù)难菌。
2.4.2.1 HTML文法定義
W3C組織指定了規(guī)范,定義了HTML的詞匯表和語(yǔ)法蔑滓。
2.4.2.2 非上下文無(wú)關(guān)的文法
我們?cè)凇敖馕鼋榻B”中提到過(guò)郊酒,上下文無(wú)關(guān)的文法可以用類似BNF的格式來(lái)定義。
不幸的是键袱,所有的傳統(tǒng)解析方式都不適用于HTML(我提出它們并不是因?yàn)楹猛媪蔷剑鼈儗⒂脕?lái)解析CSS和JavaScript),HTML不能簡(jiǎn)單地用解析所需的上下文無(wú)關(guān)文法來(lái)定義蹄咖。HTML有一個(gè)正式的格式定義——DTD(Document Type Definition)——但它并不是上下文無(wú)關(guān)的文法褐健。
HTML更接近于XML,下載有很多可用的XML解析器比藻,HTML有個(gè)XML版本的變種——XHTML铝量,那二者之間最大的不同是什么?不同之處在于HTML更加“寬容”银亲,它允許你忽略一些特定的標(biāo)簽慢叨,有時(shí)可以省略開(kāi)始或結(jié)束標(biāo)簽∥耱穑總體來(lái)說(shuō)拍谐,它是一種柔軟的語(yǔ)法,不同于XML呆板固執(zhí)的語(yǔ)法馏段。
很顯然轩拨,這個(gè)看起來(lái)很小的差異卻帶來(lái)了很大的不同。一方面院喜,這就是導(dǎo)致HTML流行的原因——它對(duì)錯(cuò)誤的寬容亡蓉,使得Web開(kāi)發(fā)者工作更輕松;但另一方面喷舀,這使得要寫(xiě)一個(gè)格式化的文法變得更加困難砍濒。所以,總結(jié)一下硫麻,解析HTML并不簡(jiǎn)單爸邢,它既不能用傳統(tǒng)的解析器解析,因?yàn)樗皇巧舷挛臒o(wú)關(guān)的文法拿愧,也不能用XML解析器解析杠河。
2.4.2.3 HTML DTD
HTML是用DTD格式進(jìn)行定義的,這種格式被用于定義SGML家族的語(yǔ)言。這種格式包括了對(duì)所有允許的元素券敌、它們的屬性以及層次關(guān)系的定義唾戚。正如前面提到的,HTML DTD不會(huì)生成一種上下文無(wú)關(guān)的文法陪白。
DTD有一些變種颈走,嚴(yán)格模式完全遵守規(guī)范,但其他模式包含對(duì)過(guò)去瀏覽器所使用標(biāo)簽的支持咱士,這么做是為了兼容以前的內(nèi)容立由。最新的嚴(yán)格DTD在此:strict.dtd。
2.4.2.4 DOM
輸出的樹(shù)也是解析樹(shù)序厉,由DOM元素和屬性節(jié)點(diǎn)組成锐膜。DOM是Document Object Model (文檔對(duì)象模型)的縮寫(xiě),它既是HTML文檔的對(duì)象表示弛房,也是HTML元素對(duì)外部的接口供JavaScript等調(diào)用道盏。樹(shù)的根是“Document”對(duì)象。
DOM和標(biāo)簽基本是一一對(duì)應(yīng)的關(guān)系文捶。如下的標(biāo)簽:
<html>
<body>
<p>Hello World</p>
<div><x:img src="example.png" /></div>
</body>
</html>
<!--譯注:請(qǐng)把x:img去掉x:荷逞,因?yàn)閱为?dú)使用img標(biāo)簽。簡(jiǎn)書(shū)markdown會(huì)把它當(dāng)作圖片上傳到服務(wù)器粹排,為了寫(xiě)作方便种远,才加上x(chóng):img的-->
將會(huì)被轉(zhuǎn)換為下面的DOM樹(shù):
和HTML一樣,DOM規(guī)范也是由W3C組織制定的顽耳,這是操作文檔的一般規(guī)范坠敷,一個(gè)特定的模塊描述一種特定的HTML元素。HTML的定義請(qǐng)查閱這里:idl-definitions.html射富。
當(dāng)我說(shuō)樹(shù)包含了DOM節(jié)點(diǎn)桨啃,我的意思是說(shuō)該樹(shù)是由實(shí)現(xiàn)了DOM接口的元素構(gòu)建而成的敛腌,瀏覽器使用已被瀏覽器內(nèi)部使用的其他屬性的具體實(shí)現(xiàn)。
2.4.2.5 解析算法
正如前面章節(jié)中所討論的暗膜,HTML不能被一般的自頂向下或自底向上的解析器所解析簇搅。原因如下:
- 語(yǔ)言本身的寬容性泽篮。
- 瀏覽器對(duì)一些常見(jiàn)的無(wú)效HTML有容錯(cuò)支持悍汛。
- 解析過(guò)程是往返的蹭越。通常情況下,源碼不會(huì)在解析過(guò)程中發(fā)生改變弛槐,但在HTML中,腳本標(biāo)簽包含的
document.write
可能添加額外標(biāo)簽依啰,所以在解析過(guò)程中實(shí)際上修改了輸入乎串。
不能使用正則解析技術(shù),瀏覽器為了解析HTML,創(chuàng)建了專屬的解析器叹誉。
HTML5規(guī)范中詳細(xì)描述了這個(gè)解析算法鸯两,它由兩個(gè)階段組成——符號(hào)化和構(gòu)建樹(shù)。符號(hào)化階段進(jìn)行詞法分析长豁,將輸入解析為符號(hào)钧唐。HTML符號(hào)包括開(kāi)始標(biāo)簽、結(jié)束標(biāo)簽匠襟、屬性名以及屬性值钝侠。符號(hào)識(shí)別器識(shí)別出符號(hào)后,會(huì)將它傳給樹(shù)構(gòu)建器酸舍,并讀取下一個(gè)字符以識(shí)別下一個(gè)符號(hào)帅韧,循環(huán)此過(guò)程,直到處理完所有的輸入啃勉。
2.4.2.6 符號(hào)識(shí)別算法
符號(hào)識(shí)別算法的輸出是HTML符號(hào),該算法用狀態(tài)機(jī)表示淮阐。每個(gè)狀態(tài)讀取輸入流中的一個(gè)或多個(gè)字符叮阅,并根據(jù)這些字符轉(zhuǎn)移到下個(gè)狀態(tài)。當(dāng)前符號(hào)的狀態(tài)以及構(gòu)建樹(shù)的狀態(tài)共同影響結(jié)果泣特,這意味著讀取同樣的字符浩姥,可能因?yàn)楫?dāng)前狀態(tài)的不同,會(huì)得到不同的結(jié)果群扶,以進(jìn)入下個(gè)正確的狀態(tài)及刻。這個(gè)算法太復(fù)雜以至于不能講解透徹,這里用一個(gè)簡(jiǎn)單的例子來(lái)幫助我們理解原理竞阐。
基本示例——符號(hào)化下面的HTML:
<html>
<body>
Hello World
</body>
</html>
初始狀態(tài)是“Data State”缴饭,當(dāng)遇到“<”字符,狀態(tài)轉(zhuǎn)變?yōu)椤?strong>Tag Open State”骆莹,讀取一個(gè)“a-z”的字符會(huì)產(chǎn)生一個(gè)開(kāi)始標(biāo)簽符號(hào)颗搂,狀態(tài)相應(yīng)地轉(zhuǎn)變?yōu)椤?strong>Tag Name State”,一直保持這個(gè)狀態(tài)幕垦,直到讀取到“>”字符丢氢,每個(gè)字符都會(huì)追加到這個(gè)符號(hào)名上,在本例中先改,我們創(chuàng)建了一個(gè)符號(hào)“html”疚察。
當(dāng)讀取到“>”字符,當(dāng)前的符號(hào)就處理完了仇奶,此時(shí)狀態(tài)就切回“Data State”了貌嫡,“<body>”標(biāo)簽會(huì)重復(fù)這一處理過(guò)程。到這里“html”和“body”標(biāo)簽都識(shí)別出來(lái)了。現(xiàn)在我們又回到“Data State”岛抄,讀取字符“H”將創(chuàng)建并識(shí)別出一個(gè)字符符號(hào)别惦,我們會(huì)為“Hello World”中的每個(gè)字符生成一個(gè)字符符號(hào),直到遇到“</body>”中的“<”夫椭。
現(xiàn)在我們又回到了“Tag Open State”掸掸,讀取下個(gè)輸入字符“/”將創(chuàng)建一個(gè)“閉合標(biāo)簽符號(hào)”,并且狀態(tài)轉(zhuǎn)移到“Tag Name State”蹭秋,再一次保持這個(gè)狀態(tài)直到遇到“>”扰付。然后會(huì)產(chǎn)生一個(gè)新的標(biāo)簽符號(hào),并回到“Data State”感凤∶踔埽“</html>”標(biāo)簽的處理跟前面一樣。
2.4.2.7 構(gòu)建樹(shù)算法
當(dāng)解析開(kāi)始時(shí)文檔對(duì)象也創(chuàng)建了陪竿,在樹(shù)的構(gòu)建階段禽翼,以Document為根的DOM將被修改,元素會(huì)被添加到樹(shù)上族跛。每個(gè)被符號(hào)識(shí)別器識(shí)別出的節(jié)點(diǎn)都會(huì)被樹(shù)構(gòu)造器處理闰挡,規(guī)范中定義了每個(gè)符號(hào)相對(duì)應(yīng)的DOM元素,該符號(hào)對(duì)應(yīng)的DOM元素會(huì)被創(chuàng)建出來(lái)礁哄。除了將元素添加到DOM樹(shù)上长酗,還會(huì)將它添加到開(kāi)發(fā)元素堆棧中。這個(gè)堆棧是用來(lái)糾正嵌套未匹配和未閉合的標(biāo)簽桐绒。這個(gè)算法也是用狀態(tài)機(jī)來(lái)描述夺脾,所有的狀態(tài)采用“插入模式”。
對(duì)于這個(gè)示例輸入茉继,我們來(lái)分析下它的樹(shù)構(gòu)造過(guò)程:
<html>
<body>
Hello World
</body>
</html>
構(gòu)建樹(shù)這一階段的輸入是符號(hào)識(shí)別階段生成的符號(hào)序列咧叭。初始化模式是“initial mode”,接收到html符號(hào)將轉(zhuǎn)移到“before html”模式烁竭,在這個(gè)模式中會(huì)對(duì)這個(gè)符號(hào)進(jìn)行再處理菲茬。這會(huì)創(chuàng)建一個(gè)HTMLHtmlElement元素并且將它附加到根元素Document對(duì)象上。
當(dāng)接收到body符號(hào)時(shí)派撕,狀態(tài)會(huì)轉(zhuǎn)移到“before head”婉弹,即使這里沒(méi)有head符號(hào),也會(huì)隱式創(chuàng)建一個(gè)HTMLHeadElement元素并添加到樹(shù)上终吼。我們現(xiàn)在轉(zhuǎn)移到“in head”模式镀赌,然后轉(zhuǎn)移到“after head”。至此际跪,body符號(hào)會(huì)被再處理商佛,將創(chuàng)建一個(gè)HTMLBodyElement并插入到樹(shù)中蛙粘,同時(shí)狀態(tài)會(huì)遷移到“in body”模式。
現(xiàn)在接受到字符串“Hello World”的字符符號(hào)威彰,第一個(gè)字符將導(dǎo)致創(chuàng)建并插入一個(gè)文本節(jié)點(diǎn),其他的字符將附加到該節(jié)點(diǎn)上穴肘。
接收到body結(jié)束符號(hào)時(shí)將轉(zhuǎn)移到“after body”模式歇盼,接著我們接收到html結(jié)束符號(hào)時(shí)狀態(tài)就轉(zhuǎn)移到“after after body”模式。當(dāng)接收到文件結(jié)束符時(shí)评抚,整個(gè)解析過(guò)程就結(jié)束了豹缀。
2.4.2.8 解析結(jié)束時(shí)的處理
在這個(gè)階段瀏覽器將文檔標(biāo)記為可交互的,并開(kāi)始解析處于延時(shí)模式中的腳本——這些腳本在文檔解析后執(zhí)行慨代。然后文檔狀態(tài)將被設(shè)置為完成邢笙,同時(shí)觸發(fā)一個(gè)load事件。
符號(hào)化和構(gòu)建樹(shù)的完整算法侍匙,請(qǐng)參考HTML5 規(guī)范氮惯。
2.4.2.9 瀏覽器容錯(cuò)機(jī)制
你從來(lái)不會(huì)在一個(gè)HTML頁(yè)面上看到“無(wú)效語(yǔ)法”這樣的錯(cuò)誤,因?yàn)闉g覽器修復(fù)了無(wú)效內(nèi)容并繼續(xù)工作想暗。以下面這段HTML為例:
<html>
<mytag></mytag>
<div><p></div>Really lousy HTML</p>
</html>
這段HTML代碼違反了很多規(guī)則(“mytag”不是合法的標(biāo)簽妇汗,“p”和“div”錯(cuò)誤的嵌套等),但瀏覽器沒(méi)有任何怨言地繼續(xù)顯示说莫,它在解析過(guò)程中修復(fù)了HTML作者的錯(cuò)誤杨箭。
瀏覽器具有一致的錯(cuò)誤處理能力,但令人驚訝的是储狭,這并不是當(dāng)前HTML規(guī)范中的內(nèi)容互婿,就像書(shū)簽和前進(jìn)/后退按鈕一樣,它只是瀏覽器長(zhǎng)期發(fā)展的結(jié)果辽狈。一些比較知名的非法HTML結(jié)構(gòu)在許多站點(diǎn)出現(xiàn)過(guò)慈参,瀏覽器都試著以一種和其他瀏覽器一致的方式去修復(fù)它們。
HTML5規(guī)范確實(shí)定義了這方面的需求稻艰,Webkit在HTML解析類開(kāi)頭的注視中懂牧,做了很好的總結(jié)。
解析器將符號(hào)化的輸入解析為文檔尊勿,構(gòu)建文檔樹(shù)僧凤。如果文檔是格式良好的,解析過(guò)程就很簡(jiǎn)單元扔。但不幸的是躯保,我們必須處理許多非格式良好的HTML文檔,因此解析器必須能容忍錯(cuò)誤澎语。我們至少應(yīng)該小心以下幾種錯(cuò)誤情況:
- 在未閉合的標(biāo)簽中途事,添加明令禁止的元素验懊。在這種情況下應(yīng)該先將前面的標(biāo)簽關(guān)閉,然后將禁止的標(biāo)簽添加到它的后面尸变。
- 不能直接添加元素义图。有些人在寫(xiě)文檔的時(shí)候會(huì)忘了一些中間標(biāo)簽(或者中間標(biāo)簽是可選的),比如:HTML HEAD BODY TR TD LI等召烂。
- 想在行內(nèi)元素中添加塊狀元素碱工,必須先關(guān)閉所有的行內(nèi)元素,直到下一個(gè)更高的塊狀元素奏夫。
- 如果這些都不行怕篷,就閉合當(dāng)前標(biāo)簽直到我們?cè)试S添加該元素或忽略該標(biāo)簽。
下面來(lái)看一些Webkit容錯(cuò)的例子:
</br>
代替<br>
一些網(wǎng)站為了兼容IE和Firefox酗昼,使用</br>
代替<br>
廊谓,Webkit統(tǒng)一將它們視為<br>
。代碼實(shí)現(xiàn)如下:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
注意:這里的錯(cuò)誤處理在內(nèi)部進(jìn)行麻削,用戶看不到蒸痹。
亂入的表格
亂入的表格是指一個(gè)表格嵌套在另一個(gè)表格中,并且還不是在它的某個(gè)單元格內(nèi)呛哟。比如下面這個(gè)例子:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
Webkit會(huì)把嵌套的表格變成兩個(gè)兄弟表格:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
代碼實(shí)現(xiàn)如下:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
嵌套的表單元素
這種情況是指用戶將一個(gè)表單嵌套到另一個(gè)表單中电抚,則第二個(gè)表單會(huì)被忽略掉。代碼實(shí)現(xiàn)如下:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
太深的標(biāo)簽繼承
代碼注視說(shuō)的很明白竖共。
http://www.liceo.edu.mx 是一個(gè)嵌套層次過(guò)深的網(wǎng)站示例蝙叛,它實(shí)現(xiàn)了約1500個(gè)標(biāo)簽的嵌套,全都來(lái)自一大堆的
<b>
標(biāo)簽公给。我們最多只允許20個(gè)相同類型的標(biāo)簽嵌套借帘,多出來(lái)的將被忽略。
實(shí)現(xiàn)代碼如下:
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;
}
放錯(cuò)地方的html或body結(jié)束標(biāo)簽
代碼注視又一次解釋地很清楚淌铐。
支持不完整的HTML肺然。我們從來(lái)不閉合body標(biāo)簽,因?yàn)橐恍┯薮赖木W(wǎng)頁(yè)總是在還未真正結(jié)束的時(shí)候就閉合它腿准。我們依賴調(diào)用
end()
方法來(lái)執(zhí)行關(guān)閉的處理际起。
代碼實(shí)現(xiàn)如下:
if (t->tagName == htmlTag || t->tagName == bodyTag)
return ;
所以Web開(kāi)發(fā)者要小心了,除非你想成為Webkit容錯(cuò)代碼的示例吐葱,否則還是寫(xiě)格式良好的HTML吧街望。
2.4.3 CSS 解析
還記得簡(jiǎn)介中提到的解析的概念嗎?不像HTML弟跑,CSS屬于上下文無(wú)關(guān)的文法灾前,因此可以用前面描述的解析器來(lái)解析。事實(shí)上CSS規(guī)范定義了CSS的詞法和語(yǔ)法文法孟辑。
看如下這個(gè)例子哎甲,每個(gè)符號(hà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)識(shí)符的縮寫(xiě)蔫敲,相當(dāng)于一個(gè)類名;“name”是一個(gè)元素的ID(用“#”引用)炭玫。
語(yǔ)法用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*] ')' ]
;
解釋:一個(gè)ruleset是這樣的結(jié)構(gòu):
div.error, a.error {
color: red;
font-weight: bold;
}
div.error
和a.error
是選擇器奈嘿,大括號(hào)中的內(nèi)容包含了這條ruleset中的規(guī)則,這個(gè)結(jié)構(gòu)在下面的定義中正式定義了:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
這說(shuō)明吞加,一個(gè)ruleset具有一個(gè)或多個(gè)選擇器指么,這些選擇器用逗號(hào)和空格(S表示空格)進(jìn)行分隔。每個(gè)ruleset包含花括號(hào)以及花括號(hào)中的一條或多條以分號(hào)隔開(kāi)的聲明榴鼎。“declaration”和“selector”的定義在后面的BNF定義中晚唇。
2.4.3.1 Webkit CSS 解析器
Webkit使用Flex和Bison解析器生成器從CSS文法文件中自動(dòng)生成解析器巫财。回憶一下解析器的介紹哩陕,Bison創(chuàng)建了一個(gè)自底向上的遞進(jìn)解析器平项。Firefox自己寫(xiě)了一個(gè)自頂向下的解析器。兩種解析器都會(huì)把每個(gè)CSS文件解析成樣式表對(duì)象(StyleSheet Object)悍及,每個(gè)對(duì)象都包含CSS規(guī)則闽瓢。CSS規(guī)則對(duì)象包含選擇器和聲明對(duì)象,以及其他與CSS語(yǔ)法對(duì)應(yīng)的對(duì)象心赶。
2.4.4 腳本解析
本章將介紹如何處理JavaScript扣讼。
2.4.5 處理腳本和樣式表的順序
2.4.5.1 腳本
Web模式是同步的,開(kāi)發(fā)者希望當(dāng)解析到一個(gè)script標(biāo)簽時(shí)缨叫,能立即解析執(zhí)行腳本椭符,文檔的解析會(huì)被阻塞,直到腳本執(zhí)行完耻姥。如果腳本是外引的销钝,則必須通過(guò)網(wǎng)絡(luò)請(qǐng)求到該資源——這個(gè)過(guò)程也是同步的,也會(huì)阻塞文檔的解析直到資源被請(qǐng)求到琐簇。這個(gè)模式保持了很多年蒸健,并且在HTML4和HTML5規(guī)范中都特別指定了。開(kāi)發(fā)者可以將腳本標(biāo)記為“defer”婉商,這樣它就不會(huì)阻塞文檔的解析似忧,并且會(huì)在文檔解析結(jié)束后執(zhí)行。HTML5新增了標(biāo)記腳本為異步的選項(xiàng)丈秩,這樣會(huì)使用另一個(gè)線程解析執(zhí)行腳本橡娄。
2.4.5.2 預(yù)解析
Webkit和Firefox都做了這個(gè)優(yōu)化,當(dāng)執(zhí)行腳本時(shí)癣籽,另一個(gè)線程解析剩下的文檔挽唉,并加載后面需要通過(guò)網(wǎng)絡(luò)加載的資源滤祖。這種方式可以使資源并行加載從而提高整體的速度。值得注意的是瓶籽,預(yù)解析并不會(huì)修改DOM樹(shù)匠童,它將這個(gè)工作留給主解析器,它只會(huì)解析外部資源引用塑顺,比如外部腳本汤求、樣式表以及圖片。
2.4.5.3 樣式表
樣式表采用另一種不同的模式严拒。理論上來(lái)說(shuō)扬绪,既然樣式表不會(huì)改變DOM樹(shù),就沒(méi)必要停下文檔的解析等待它們裤唠。然而存在一個(gè)問(wèn)題挤牛,在文檔的解析過(guò)程中腳本可能會(huì)請(qǐng)求樣式信息,如果樣式還沒(méi)有加載和解析种蘸,腳本將得到錯(cuò)誤的結(jié)果墓赴,很明顯這將會(huì)導(dǎo)致很多問(wèn)題。這看起來(lái)是個(gè)邊緣情況航瞭,但確實(shí)很常見(jiàn)诫硕。Firefox在樣式表加載和解析的時(shí)候會(huì)阻塞所有的腳本,而Chrome只有在當(dāng)腳本訪問(wèn)某些未加載的樣式表所影響的特定的樣式屬性時(shí)刊侯,才阻塞這些腳本章办。
2.5 渲染樹(shù)的構(gòu)造
當(dāng)DOM樹(shù)構(gòu)建完后,瀏覽器開(kāi)始構(gòu)建另一棵樹(shù)——渲染樹(shù)滨彻。渲染樹(shù)是由元素顯示序列中的可見(jiàn)元素組成纲菌,它是文檔的可視化表示,構(gòu)建這棵樹(shù)是為了以正確的順序繪制文檔的內(nèi)容疮绷。
Firefox將渲染樹(shù)中的元素稱為“frames”翰舌,Webkit則使用術(shù)語(yǔ)“renderer”或“render object”。一個(gè)renderer知道怎么布局以及繪制自己和它的子元素冬骚。
RenderObject是Webkit渲染對(duì)象的基類椅贱,它的定義如下:
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
}
每個(gè)渲染對(duì)象代表一個(gè)矩形區(qū)域,這個(gè)矩形區(qū)域通常與該節(jié)點(diǎn)的CSS盒模型相對(duì)應(yīng)只冻,在CSS2規(guī)范中定義了該盒模型庇麦,它包含諸如寬、高和位置之類的幾何信息喜德。
盒模型的類型受對(duì)應(yīng)節(jié)點(diǎn)的display
樣式屬性的影響(參考樣式計(jì)算章節(jié))山橄。下面的Webkit代碼說(shuō)明了如何根據(jù)display
屬性決定為某個(gè)節(jié)點(diǎn)創(chuàng)建何種類型的渲染對(duì)象。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->renderArena();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch(style->display()) {
case NODE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderInlineBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
當(dāng)然舍悯,元素的類型也需要考慮航棱,例如表單控件和表格帶有特殊的框架睡雇。在Webkit中,如果一個(gè)元素想要?jiǎng)?chuàng)建特殊的渲染對(duì)象饮醇,它需要重寫(xiě)createRenderer
方法它抱,使渲染對(duì)象指向不包含幾何信息的樣式對(duì)象。
2.5.1 渲染樹(shù)和DOM樹(shù)的關(guān)系
渲染對(duì)象和DOM元素相對(duì)應(yīng)朴艰,但這種關(guān)系不是一一對(duì)應(yīng)的观蓄。不可見(jiàn)的DOM元素不會(huì)被插入到渲染樹(shù),例如“head”元素祠墅。此外侮穿,display
屬性為none
的元素也不會(huì)出現(xiàn)在渲染樹(shù)中(visibility
屬性為hidden
的元素會(huì)出現(xiàn)在渲染樹(shù)中)。
有一些DOM元素對(duì)應(yīng)幾個(gè)可見(jiàn)對(duì)象毁嗦,它們一般是具有復(fù)雜結(jié)構(gòu)的元素亲茅,無(wú)法用一個(gè)矩形來(lái)描述。例如金矛,“select”元素有3個(gè)渲染對(duì)象——一個(gè)顯示區(qū)域、一個(gè)下拉列表和一個(gè)按鈕勺届。同樣驶俊,當(dāng)文本因?yàn)閷挾炔粔蚨鴵Q行時(shí),新行將作為額外的元素被添加免姿。另一個(gè)多渲染對(duì)象的例子是不規(guī)范的HTML饼酿。根據(jù)CSS規(guī)范,一個(gè)行內(nèi)元素只能僅包含行內(nèi)元素或僅包含塊狀元素胚膊,在存在混合內(nèi)容時(shí)故俐,將會(huì)創(chuàng)建匿名的塊狀渲染對(duì)象包裹住行內(nèi)元素。
一些渲染對(duì)象和所對(duì)應(yīng)的DOM節(jié)點(diǎn)不在樹(shù)上相同的位置紊婉。例如药版,浮動(dòng)和絕對(duì)定位的元素在正常流之外,在兩棵樹(shù)上的位置不同喻犁,渲染樹(shù)上標(biāo)志出真實(shí)的結(jié)構(gòu)槽片,并用一個(gè)占位結(jié)構(gòu)標(biāo)志出它們?cè)瓉?lái)的位置。
2.5.2 創(chuàng)建樹(shù)的流程
Firefox中,表現(xiàn)為注冊(cè)DOM更新的監(jiān)聽(tīng)器传轰,將frame的創(chuàng)建委派給“FrameConstructor”剩盒,這個(gè)構(gòu)建器解析樣式(參考樣式計(jì)算)并創(chuàng)建一個(gè)frame。
在Webkit中慨蛙,解析樣式并生成渲染對(duì)象的過(guò)程稱為“attachment”辽聊,每個(gè)DOM節(jié)點(diǎn)都有一個(gè)“attach”方法纪挎,attachment的過(guò)程是同步的,調(diào)用新節(jié)點(diǎn)的attach方法將節(jié)點(diǎn)插入到DOM樹(shù)中身隐。
處理html和body標(biāo)簽將構(gòu)建渲染樹(shù)的根廷区,這個(gè)根渲染對(duì)象對(duì)應(yīng)CSS規(guī)范中的“containing block”——包含其他所有blocks的頂級(jí)block。它的大小就是viewport——瀏覽器窗口的顯示區(qū)域贾铝。Firefox稱它為“ViewPortFrame”隙轻,而Webkit稱它為“RenderView”。這個(gè)就是文檔所指的渲染對(duì)象垢揩,樹(shù)中其他部分將作為插入的DOM節(jié)點(diǎn)被創(chuàng)建玖绿。詳細(xì)內(nèi)容,請(qǐng)參閱CSS2的相關(guān)主題——Processing Model叁巨。
2.5.3 樣式計(jì)算
構(gòu)建渲染樹(shù)需要計(jì)算出每個(gè)渲染對(duì)象的可視屬性斑匪,這可以通過(guò)計(jì)算每個(gè)元素的樣式屬性得到。
樣式包括各種來(lái)源的樣式表锋勺,行內(nèi)樣式元素以及html中的可視化屬性(如“bgcolor”)蚀瘸,之后會(huì)將它轉(zhuǎn)化為CSS樣式屬性。
樣式表來(lái)源于瀏覽器的默認(rèn)樣式表庶橱,頁(yè)面作者提供的樣式表和用戶提供的樣式表——這些樣式表是瀏覽器用戶提供的(瀏覽器允許用戶自定義喜歡的樣式贮勃。例如,在Firefox中苏章,可以通過(guò)在“Firefox Profile”目錄下放置樣式表來(lái)實(shí)現(xiàn))寂嘉。
樣式計(jì)算有一些困難:
- 樣式數(shù)據(jù)是一個(gè)非常大的結(jié)構(gòu),保存大量的樣式屬性會(huì)導(dǎo)致內(nèi)存問(wèn)題枫绅。
- 如果不進(jìn)行優(yōu)化泉孩,找到每個(gè)元素匹配的規(guī)則會(huì)導(dǎo)致性能問(wèn)題,為每個(gè)元素查找匹配的規(guī)則都需要遍歷整個(gè)規(guī)則表并淋,這個(gè)工作量非常大寓搬。選擇器可能有復(fù)雜的結(jié)構(gòu)灼捂,匹配過(guò)程如果沿著一條開(kāi)始看似正確售碳,后來(lái)被證明是無(wú)用的路徑,則必須去嘗試另一條路徑盟榴。例如下面這個(gè)復(fù)雜的選擇器:
div div div div {
...
}
這意味著需要把規(guī)則應(yīng)用到三個(gè)div的后代div元素上酬诀,假設(shè)你想要檢查該規(guī)則是否已應(yīng)該應(yīng)用到某個(gè)給定的“<div>”元素上脏嚷,你選擇樹(shù)上一條特定的路徑去檢查,這可能需要遍歷節(jié)點(diǎn)樹(shù)瞒御,最后卻發(fā)現(xiàn)它只是兩個(gè)div的后臺(tái)父叙,并不能應(yīng)用該規(guī)則。然后你不得不嘗試另外一條路徑。
- 應(yīng)用規(guī)則涉及非常復(fù)雜的級(jí)聯(lián)規(guī)則趾唱,它們定義了規(guī)則的層次涌乳。
讓我們來(lái)看一下瀏覽器是如何處理這些問(wèn)題的:
2.5.3.1 共享樣式數(shù)據(jù)
Webkit節(jié)點(diǎn)引用的樣式對(duì)象(RenderStyle)在某些情況下可以被節(jié)點(diǎn)間共享,這些節(jié)點(diǎn)必須是兄弟或者表兄弟節(jié)點(diǎn)甜癞,并且滿足以下條件:
- 這些元素必須處于相同的鼠標(biāo)狀態(tài)(比如:不能一個(gè)處于hover夕晓,另一個(gè)不是)
- 元素不能具有ID
- 標(biāo)簽名必須匹配
- class屬性必須匹配
- 映射的屬性集必須是相同的
- 鏈接的狀態(tài)必須匹配
- 焦點(diǎn)的狀態(tài)必須匹配
- 不能有元素被屬性選擇器影響
- 元素不能有行內(nèi)樣式屬性
- 不能使用兄弟選擇器,WebCore在遇到兄弟選擇器時(shí)悠咱,只是簡(jiǎn)單地拋出一個(gè)全局轉(zhuǎn)換蒸辆,并且在它們顯示時(shí)使整個(gè)文檔的樣式共享失效,這些包括
+
選擇器和類似:first-child
和:last-child
這樣的選擇器
2.5.3.2 Firefox 規(guī)則樹(shù)
Firefox用兩棵樹(shù)來(lái)簡(jiǎn)化樣式計(jì)算——規(guī)則樹(shù)和樣式上下文樹(shù)析既。Webkit也有樣式對(duì)象躬贡,但它們并沒(méi)有存儲(chǔ)在類似上下文樹(shù)這樣的樹(shù)中,只是由DOM節(jié)點(diǎn)指向其關(guān)聯(lián)的樣式眼坏。
樣式上下文包含最終值拂玻,這些值是通過(guò)以正確的順序應(yīng)用所有匹配的規(guī)則,并將它們由邏輯值轉(zhuǎn)換為具體的值宰译。例如檐蚜,如果邏輯值是屏幕百分比,則通過(guò)計(jì)算將其轉(zhuǎn)換為絕對(duì)單位沿侈。使用規(guī)則樹(shù)這個(gè)注意確實(shí)很巧妙闯第,它允許節(jié)點(diǎn)中共享這些只,而不需要重復(fù)計(jì)算肋坚,同時(shí)也節(jié)省了存儲(chǔ)空間乡括。
所有匹配的規(guī)則都存儲(chǔ)在規(guī)則樹(shù)中肃廓,一條路徑中的最底層節(jié)點(diǎn)擁有最高的優(yōu)先級(jí)智厌,這棵樹(shù)包含了已經(jīng)找到的所有匹配規(guī)則的路徑。存儲(chǔ)規(guī)則是懶加載的盲赊,規(guī)則樹(shù)并不是一開(kāi)始就為每個(gè)節(jié)點(diǎn)進(jìn)行計(jì)算铣鹏,而是在某個(gè)節(jié)點(diǎn)需要計(jì)算樣式的時(shí)候才進(jìn)行相應(yīng)的計(jì)算,并將計(jì)算后的路徑添加到樹(shù)中哀蘑。
我們將樹(shù)上的路徑看成詞典中的單詞诚卸,假如已經(jīng)計(jì)算出了如下的規(guī)則樹(shù):
假如要為內(nèi)容樹(shù)中的另一個(gè)節(jié)點(diǎn)匹配規(guī)則,現(xiàn)在知道匹配的規(guī)則(以正確的順序)是”B-E-I”绘迁,因?yàn)槲覀円呀?jīng)計(jì)算出了路徑“A-B-E-I-L”合溺,所以樹(shù)上已經(jīng)存在這條路徑,現(xiàn)在剩下的工作就很少了缀台。
現(xiàn)在來(lái)看下樹(shù)是如何保存工作的棠赛。
2.5.3.2.1 結(jié)構(gòu)化
樣式上下文按結(jié)構(gòu)進(jìn)行劃分,這些結(jié)構(gòu)包含類似border
或color
這樣的特定分類的樣式信息。結(jié)構(gòu)中的所有屬性不是繼承的就是非繼承的睛约,對(duì)繼承的屬性鼎俘,除非元素自身有定義,否則就從它的parent那繼承辩涝。非繼承的屬性(又稱“reset”屬性)如果沒(méi)有定義贸伐,則使用默認(rèn)值。
樣式上下文樹(shù)通過(guò)緩存完整的結(jié)構(gòu)(包含計(jì)算后的值)來(lái)幫助我們怔揩,這樣如果底層節(jié)點(diǎn)沒(méi)有為一個(gè)結(jié)構(gòu)提供定義捉邢,則使用上層節(jié)點(diǎn)緩存的結(jié)構(gòu)。
2.5.3.2.2 使用規(guī)則樹(shù)計(jì)算樣式上下文
當(dāng)為一個(gè)特定的元素計(jì)算樣式時(shí)沧踏,首先計(jì)算出規(guī)則樹(shù)中的一條路徑歌逢,或者使用已經(jīng)存在的一條,然后用路徑中的規(guī)則去填充新的樣式上下文結(jié)構(gòu)翘狱。從路徑的底層節(jié)點(diǎn)開(kāi)始秘案,它具有最高的優(yōu)先級(jí)(通常是最特定的選擇器),遍歷規(guī)則樹(shù)潦匈,直到填滿我們的結(jié)構(gòu)阱高。如果在那個(gè)規(guī)則節(jié)點(diǎn)沒(méi)有定義所需的結(jié)構(gòu)規(guī)則,我們就可以大大地進(jìn)行優(yōu)化——我們可以沿著樹(shù)向上查詢茬缩,直到找到一個(gè)指向該規(guī)則的節(jié)點(diǎn)——這是最好的優(yōu)化赤惊,整個(gè)結(jié)構(gòu)都被共享了,這也節(jié)省了最終值的計(jì)算和內(nèi)存凰锡。
如果我們找到部分定義未舟,我們會(huì)繼續(xù)沿著樹(shù)往上查詢,直到結(jié)構(gòu)體被填滿掂为。如果最終沒(méi)有找到該結(jié)構(gòu)的任何規(guī)則定義裕膀,那么如果這個(gè)結(jié)構(gòu)是繼承型的,我們就指向上下文樹(shù)中的parent結(jié)構(gòu)勇哗,在這種情況下昼扛,我們也成功的共享了結(jié)構(gòu);如果這個(gè)結(jié)構(gòu)是reset型的欲诺,則使用默認(rèn)的值抄谐。
如果最特定的節(jié)點(diǎn)添加了值,那么我們需要做一些額外的計(jì)算將其轉(zhuǎn)換為實(shí)際值扰法,然后將結(jié)果緩存在樹(shù)上的節(jié)點(diǎn)蛹含,這樣它就可以被子節(jié)點(diǎn)所用。
當(dāng)一個(gè)元素和它的兄弟元素指向同一個(gè)樹(shù)節(jié)點(diǎn)時(shí)塞颁,整個(gè)樣式上下文 都可以被它們共享浦箱。
來(lái)看一個(gè)例子卧斟,假如有下面這段HTML:
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error </span> error
</p>
</div>
<div class="err" id="div2"></div>
</body>
</html>
以及下面這些規(guī)則:
1. div { margin: 5px; color: black; }
2. .err { color: red; }
3. .big { margin-top: 3px; }
4. div span { margin-bottom: 4px; }
5. #div1 { color: blue; }
6. #div2 { color: green; }
簡(jiǎn)化下問(wèn)題,我們只填充兩個(gè)結(jié)構(gòu)——color和margin憎茂,color結(jié)構(gòu)只包含一個(gè)成員——顏色珍语,margin結(jié)構(gòu)包含四邊。
生成的規(guī)則樹(shù)如下(節(jié)點(diǎn)名:指向的規(guī)則):
上下文樹(shù)如下(節(jié)點(diǎn)名:指向的規(guī)則節(jié)點(diǎn)):
假如我們解析HTML碰到第二個(gè)<div>
標(biāo)簽竖幔,我們需要為這個(gè)節(jié)點(diǎn)創(chuàng)建樣式上下文板乙,并填充它的樣式結(jié)構(gòu)。我們要進(jìn)行規(guī)則匹配拳氢,發(fā)現(xiàn)這個(gè)<div>
匹配的規(guī)則為1募逞、2、6馋评,我們發(fā)現(xiàn)規(guī)則樹(shù)上已經(jīng)存在一條我們可以使用的路徑1放接、2,我們只需為規(guī)則6新增一個(gè)節(jié)點(diǎn)添加到下面(就是規(guī)則樹(shù)中的F)留特。我們會(huì)創(chuàng)建一個(gè)樣式上下文并將其放到上下文樹(shù)中纠脾,新的樣式上下文將指向規(guī)則樹(shù)中的節(jié)點(diǎn)F。
我們現(xiàn)在需要填充這個(gè)樣式的上下文蜕青,先從填充margin結(jié)構(gòu)開(kāi)始苟蹈,既然最后一個(gè)規(guī)則節(jié)點(diǎn)F沒(méi)有添加margin結(jié)構(gòu),沿著路徑向上右核,直到找到緩存的前面插入節(jié)點(diǎn)計(jì)算出的結(jié)構(gòu)慧脱,我們發(fā)現(xiàn)節(jié)點(diǎn)B是最近的指定margin值的節(jié)點(diǎn)。
因?yàn)橐呀?jīng)有了color結(jié)構(gòu)的定義贺喝,所以不能使用緩存的結(jié)構(gòu)菱鸥。既然color只有一個(gè)屬性,所以也就不需要沿著路徑向上填充其他屬性躏鱼。我們會(huì)計(jì)算出最終值(將字符串轉(zhuǎn)換為RGB等)氮采,并將計(jì)算后的結(jié)構(gòu)緩存在節(jié)點(diǎn)上。
第二個(gè)<span>
元素更簡(jiǎn)單挠他,進(jìn)行規(guī)則匹配后發(fā)現(xiàn)它指向規(guī)則G扳抽,和前一個(gè)<span>
一樣篡帕,和前一個(gè)<span>
一樣殖侵。既然有兄弟節(jié)點(diǎn)指向同一個(gè)節(jié)點(diǎn),就可以共享整個(gè)樣式上下文镰烧,只需指向前一個(gè)<span>
的上下文拢军。
因?yàn)榻Y(jié)構(gòu)中包含繼承自parent的規(guī)則,上下文樹(shù)做了緩存(color屬性是繼承來(lái)的怔鳖,但Firefox將其視為reset并在規(guī)則樹(shù)中緩存)茉唉。例如,如果我們?yōu)橐粋€(gè)段落添加如下規(guī)則:
p { font-family: Verdana; font-size: 10px; font-weight: bold; }
那么這個(gè)<p>
在內(nèi)容樹(shù)中的子節(jié)點(diǎn)<div>
,會(huì)共享和它parent一樣的font結(jié)構(gòu)度陆,這種情況發(fā)生在沒(méi)有為這個(gè)<div>
指定font規(guī)則時(shí)艾凯。
在Webkit中并沒(méi)有規(guī)則樹(shù),匹配聲明會(huì)被遍歷四次懂傀。首先應(yīng)用非important
的高優(yōu)先級(jí)屬性(之所以先應(yīng)用這些屬性趾诗,是因?yàn)槠渌蕾囉谒鼈儯热纾?code>display屬性)蹬蚁,其次是高優(yōu)先級(jí)important
恃泪,接著是一般優(yōu)先級(jí)的非important
,最后是一般優(yōu)先級(jí)的important
的規(guī)則犀斋。這意味著出現(xiàn)多次的屬性將被按照正確的級(jí)聯(lián)順序進(jìn)行處理贝乎,最后一個(gè)生效。
總結(jié)一下叽粹,共享樣式對(duì)象(整個(gè)結(jié)構(gòu)或結(jié)構(gòu)的部分屬性)解決了問(wèn)題1和3览效。Firefox的規(guī)則樹(shù)也對(duì)以正確順序應(yīng)用規(guī)則起到幫助。
2.5.3.3 處理規(guī)則以簡(jiǎn)化匹配
樣式規(guī)則有幾個(gè)來(lái)源:
- 來(lái)自外部樣式表或
<style>
標(biāo)簽中的CSS規(guī)則虫几,如:p { color: blue; }
- 行內(nèi)樣式屬性朽肥,如:
<p style="color: blue"></p>
- HTML可視化屬性(映射為對(duì)應(yīng)的樣式規(guī)則),如:
<p bgcolor="blue"></p>
后面兩個(gè)很容易匹配到元素持钉,因?yàn)樗鼈兯鶕碛械臉邮綄傩院虷TML屬性可以將元素作為key進(jìn)行映射衡招。
就像前面問(wèn)題2提到的,CSS的規(guī)則匹配很復(fù)雜每强,為了解決這個(gè)問(wèn)題始腾,可以先對(duì)規(guī)則進(jìn)行處理,使其更容易訪問(wèn)空执。
解析完樣式表之后浪箭,規(guī)則會(huì)根據(jù)選擇符被添加到一些哈希表。這些表可以是根據(jù)id辨绊、class奶栖、標(biāo)簽名或任何不屬于這三個(gè)分類的通用映射表门坷。如果選擇符是id宣鄙,規(guī)則將被添加了id映射表中;如果是class默蚌,則被添加到class映射表中冻晤,等等。這個(gè)處理簡(jiǎn)化了匹配規(guī)則绸吸,沒(méi)必要查看每個(gè)聲明鼻弧,我們可以從映射表中找到一個(gè)元素的相關(guān)規(guī)則设江。這個(gè)優(yōu)化覆蓋了 95+% 的規(guī)則,在匹配過(guò)程中就可以不考慮這些規(guī)則了攘轩。
例如叉存,看下面的樣式規(guī)則:
p.error { color: red; }
#messageDiv { height: 50px; }
div { margin: 5px; }
第一條規(guī)則將被插入class映射表,第二條規(guī)則插入id映射表度帮,第三條長(zhǎng)路標(biāo)簽映射表鹉胖。對(duì)于下面這段HTML:
<p class="error">an error occurred</p>
<div id="messageDiv">this is a message</div>
我們首先找到p元素對(duì)應(yīng)的規(guī)則,class映射表包含一個(gè)“error”的key够傍,根據(jù)key可以找到p.error
的規(guī)則甫菠。div元素在id映射表(key就是對(duì)應(yīng)的id)和標(biāo)簽映射表中都有相關(guān)的規(guī)則,剩下的工作就是找出這些key對(duì)應(yīng)的規(guī)則中冕屯,哪些是真正匹配的寂诱。例如,如果div的規(guī)則是:
table div { margin: 5px; }
這個(gè)規(guī)則也是從標(biāo)簽映射表中獲得的安聘,因?yàn)閗ey是最右邊的選擇符痰洒,但它并不匹配這里的div元素,因?yàn)镠TML片段中的div并沒(méi)有table祖先浴韭。
Webkit和Firefox都會(huì)做這個(gè)處理丘喻。
2.5.3.4 以正確的級(jí)聯(lián)順序應(yīng)用規(guī)則
樣式對(duì)象的屬性對(duì)應(yīng)所有可見(jiàn)屬性(所有CSS屬性,但是更通用)念颈。如果屬性沒(méi)有被任何匹配的規(guī)則所定義泉粉,那么一些屬性可以從parent的樣式對(duì)象中繼承,另一些使用默認(rèn)值榴芳。
問(wèn)題產(chǎn)生于存在不止一處的定義嗡靡,我們可以用級(jí)聯(lián)順序來(lái)解決這個(gè)問(wèn)題。
2.5.3.4.1 樣式表的級(jí)聯(lián)順序
一個(gè)樣式屬性的聲明可能出現(xiàn)在幾個(gè)樣式表中窟感,或者在一個(gè)樣式表中出現(xiàn)多次讨彼。這意味著應(yīng)用規(guī)則的順序至關(guān)重要,這個(gè)順序就是級(jí)聯(lián)順序柿祈。根據(jù)CSS2的規(guī)范哈误,級(jí)聯(lián)順序?yàn)椋◤牡偷礁撸?/p>
- 瀏覽器的聲明
- 用戶的一般聲明
- 作者的一般聲明
- 作者的important聲明
- 用戶的important聲明
瀏覽器的聲明是最不重要的,用戶的聲明只有被標(biāo)記為important的時(shí)候才會(huì)覆蓋作者的聲明躏嚎。具有同等級(jí)別的聲明將根據(jù) 特殊性(specifity) 以及它們被定義時(shí)的順序進(jìn)行排序蜜自。HTML的可視化屬性會(huì)被轉(zhuǎn)換成匹配的CSS聲明,它們被視為最低優(yōu)先級(jí)的作者規(guī)則紧索。
2.5.3.4.2 選擇符的特殊性
CSS2規(guī)范 中定義的選擇器特殊性如下:
- 如果聲明來(lái)自style屬性袁辈,而不是一個(gè)選擇器的規(guī)則菜谣,則計(jì)為1珠漂,否則計(jì)為0(=a)
- 計(jì)算選擇器中id屬性的數(shù)量(=b)
- 計(jì)算選擇器中class以及偽類的數(shù)量(=c)
- 計(jì)算選擇器中元素名以及偽元素的數(shù)量(=d)
連接a-b-c-d
四個(gè)數(shù)字(用大基數(shù)的計(jì)算系統(tǒng))將得到選擇器的特殊性(specifity)晚缩。例如,如果a為14媳危,你可以使用16進(jìn)制荞彼;如果a為17,則需要使用十七進(jìn)制待笑,這種情況可能發(fā)生在選擇符為html body div div p ...
(選擇符中有17個(gè)標(biāo)簽鸣皂,一般不太可能)。
這里有一些特殊性計(jì)算的例子:
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
2.5.3.4.3 規(guī)則排序
規(guī)則匹配后暮蹂,需要根據(jù)級(jí)聯(lián)順序?qū)σ?guī)則進(jìn)行排序寞缝。Webkit中對(duì)小列表使用冒泡排序,大列表用歸并排序仰泻。Webkit通過(guò)為規(guī)則重載“>”操作符來(lái)執(zhí)行排序:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
2.5.4 逐步處理
Webkit使用一個(gè)標(biāo)志位標(biāo)識(shí)所有頂層樣式表(包括@imports
)是否已經(jīng)加載荆陆,如果在attaching的時(shí)候樣式表沒(méi)有完全加載,則放置占位符集侯,并在文檔中標(biāo)記被啼,一旦樣式表完成加載就重新進(jìn)行計(jì)算。
2.6 布局(Layout)
當(dāng)渲染對(duì)象被創(chuàng)建并添加到樹(shù)中棠枉,它們并沒(méi)有位置和大小浓体,計(jì)算這些值的過(guò)程稱為layout或reflow。
HTML使用基于流的布局模型辈讶,這意味著大多數(shù)時(shí)候可以以單一的途徑進(jìn)行幾何計(jì)算命浴。流中靠后的元素并不會(huì)影響前面元素的幾何特性,所以布局可以在文檔中從左到右贱除、自上而下的進(jìn)行咳促。也存在一些例外,比如HTML的table可能需要不止一行勘伺。
坐標(biāo)系統(tǒng)相對(duì)于根frame跪腹,使用top和left坐標(biāo)。
布局是個(gè)遞歸的過(guò)程飞醉,由根渲染對(duì)象開(kāi)始冲茸,它對(duì)應(yīng)HTML文檔元素。布局繼續(xù)遞歸的通過(guò)一些或所有的frame層級(jí)缅帘,為每個(gè)需要幾何信息的渲染對(duì)象進(jìn)行計(jì)算轴术。
根渲染對(duì)象的位置是0,0
,它的大小是viewport——瀏覽器窗口的可見(jiàn)部分钦无。
所有的渲染對(duì)象都有一個(gè)layout或reflow方法逗栽,每個(gè)渲染對(duì)象調(diào)用需要布局的children的layout方法。
2.6.1 臟點(diǎn)系統(tǒng)(Dirty bit system)
為了不因每個(gè)小變化都全部重新布局失暂,瀏覽器使用了“dirty bit”系統(tǒng)彼宠。如果一個(gè)渲染對(duì)象發(fā)生了變化或者被添加了鳄虱,就標(biāo)記它以及它的children都為“dirty”——需要重新布局。這里有兩種標(biāo)志——“dirty”和“children are dirty”凭峡,“Children are dirty”說(shuō)明即使這個(gè)渲染對(duì)象可能沒(méi)問(wèn)題拙已,但它至少有一個(gè)child需要重新布局。
2.6.2 全局布局和增量布局
布局在整棵渲染樹(shù)上觸發(fā)時(shí)摧冀,稱為全局布局倍踪,下面兩種情況可能發(fā)生全局布局:
- 一個(gè)全局的樣式改變影響所有的渲染對(duì)象,比如:
font-size
的改變索昂。 - 窗口resize
布局也可以是增量進(jìn)行的建车,這樣只有標(biāo)記為“dirty”的渲染對(duì)象會(huì)重新布局(也會(huì)導(dǎo)致一些額外的布局)。增量布局會(huì)在渲染對(duì)象為“dirty”的時(shí)候(異步)觸發(fā)椒惨。例如癞志,當(dāng)網(wǎng)絡(luò)接收到新的內(nèi)容并添加到DOM樹(shù)后,新的渲染對(duì)象會(huì)添加到渲染樹(shù)中框产。
2.6.3 異步布局和同步布局
增量布局是異步完成的。Firefox為增量布局生成了“reflow”隊(duì)列以及一個(gè)調(diào)度器觸發(fā)這些批處理命令秉宿。Webkit也有一個(gè)計(jì)時(shí)器用來(lái)執(zhí)行增量布局——遍歷樹(shù)并為“dirty”狀態(tài)的渲染對(duì)象重新布局戒突。此外,當(dāng)腳本請(qǐng)求樣式信息時(shí)描睦,例如:offsightHeight
膊存,會(huì)同步地觸發(fā)增量布局。全局布局一般都是同步觸發(fā)的忱叭。有的時(shí)候布局會(huì)被作為一個(gè)初始布局的回調(diào)隔崎,因?yàn)橐恍傩园l(fā)生了改變,比如滾動(dòng)條的位置發(fā)生改變韵丑。
2.6.4 優(yōu)化
當(dāng)一個(gè)布局因?yàn)閞esize或渲染位置(不是大芯糇洹)的改變而觸發(fā)時(shí),渲染對(duì)象的大小將會(huì)從緩存中讀取撵彻,而不會(huì)重新計(jì)算钓株。某些情況下,如果只有子樹(shù)被修改了陌僵,則布局并不從根開(kāi)始轴合。這種情況可能發(fā)生,比如變化發(fā)生在元素自身并且不影響它周圍元素碗短,例如將文本插入文本框中(否則每次擊鍵都將觸發(fā)從根開(kāi)始的重排)受葛。
2.6.5 布局過(guò)程
布局通常有以下幾種模式:
- Parent渲染對(duì)象決定它的寬度。
- Parent渲染對(duì)象讀取children,并且:
- 放置child渲染對(duì)象(設(shè)置它的x和y)总滩。
- 在需要時(shí)(它們當(dāng)前為“dirty”或處于全局布局狀態(tài)下或是其他原因)調(diào)用child渲染對(duì)象的layout纲堵,這將計(jì)算child的高度。
- Parent渲染對(duì)象使用child渲染對(duì)象的累積高度以及margin和padding的高度來(lái)設(shè)置自己的高度——這將被parent渲染對(duì)象的parent使用咳秉。
- 將它的“dirty”標(biāo)志設(shè)置為
false
Firefox使用一個(gè)“state”對(duì)象(nsHTMLReflowState)作為參數(shù)去布局(在Firefox中稱為reflow)婉支,state對(duì)象包含parent的寬度及其他內(nèi)容鸯隅。Firefox布局的輸出是一個(gè)“metrics”對(duì)象(nsHTMLReflowMetrics)澜建,它包括渲染對(duì)象的高度。
2.6.6 寬度計(jì)算
渲染對(duì)象的寬度使用容器的寬度蝌以、渲染對(duì)象樣式中的寬度以及margin炕舵、border進(jìn)行計(jì)算。例如跟畅,下面這個(gè)div的寬度:
<div style="width:30%"></div>
Webkit中寬度的計(jì)算過(guò)程如下(RenderBox
類的calcWidth
方法):
- 容器的寬度是容器可用寬度和0中的最大值咽筋,這里的可用寬度是內(nèi)容寬度,它等于:
contentWidth = clientWidth() - paddingLeft() - paddingRight()
徊件,clientWidth和clientHeight代表一個(gè)對(duì)象內(nèi)部不包括border和滑動(dòng)條的大小奸攻。 - 元素的寬度是指樣式屬性
width
的值,它可以通過(guò)計(jì)算父容器寬度的百分比得到一個(gè)絕對(duì)值虱痕。 - 加上水平方向上的border和padding
到此位置睹耐,這就是“最佳寬度”的計(jì)算過(guò)程,現(xiàn)在計(jì)算寬度的最大值和最小值部翘。如果最佳寬度大于最大寬度硝训,則使用最大寬度;如果最佳寬度小于最小寬度新思,則使用最小寬度窖梁。最后緩存這個(gè)值,當(dāng)需要重新布局并且寬度未改變的時(shí)候會(huì)被重復(fù)用到夹囚。
2.6.7 換行
當(dāng)一個(gè)渲染對(duì)象在布局過(guò)程中需要換行時(shí)纵刘,它會(huì)暫停并告訴它的parent它需要換行,parent會(huì)創(chuàng)建額外的渲染對(duì)象并調(diào)用它們的layout方法荸哟。
2.7 繪制(Painting)
在繪制階段彰导,會(huì)遍歷渲染樹(shù)并調(diào)用渲染對(duì)象的“paint”方法,將它們的內(nèi)容顯示在屏幕上敲茄,繪制使用UI基礎(chǔ)組件位谋,這在UI的章節(jié)會(huì)有更多的介紹。
2.7.1 全局和增量
和布局一樣堰燎,繪制也可以是全局的(繪制完整的樹(shù))或增量的掏父。在增量繪制過(guò)程中,一些渲染對(duì)象以不影響整棵樹(shù)的方式改變秆剪,發(fā)生改變的渲染對(duì)象使其在屏幕上的矩形區(qū)域失效赊淑,這會(huì)導(dǎo)致操作系統(tǒng)將其看作“dirty region”爵政,并產(chǎn)生一個(gè)“paint”事件,操作系統(tǒng)很巧妙地將多個(gè)區(qū)域合并為一個(gè)陶缺。在Chrome中這個(gè)過(guò)程更復(fù)雜點(diǎn)钾挟,因?yàn)殇秩緦?duì)象在不同的進(jìn)程中,而不是在主進(jìn)程中饱岸。Chrome在一定程度上模擬了操作系統(tǒng)的行為掺出,表現(xiàn)為監(jiān)聽(tīng)事件并派發(fā)消息給渲染根,遍歷樹(shù)直到找到相關(guān)的渲染對(duì)象苫费,重繪這個(gè)對(duì)象(通常還會(huì)重繪它的children)汤锨。
2.7.2 繪制順序
CSS2定義了繪制過(guò)程的順序,這實(shí)際上是元素壓入堆棧上下文的順序百框,這個(gè)順序影響著繪制闲礼,因?yàn)槎褩J菑暮笙蚯袄L制。一個(gè)塊渲染對(duì)象的堆棧順序是:
- background color
- background image
- border
- children
- outline
2.7.3 Firefox 顯示列表
Firefox讀取渲染樹(shù)并為繪制的矩形創(chuàng)建一個(gè)顯示列表铐维,該列表以正確的繪制順序包含這個(gè)矩形相關(guān)的渲染對(duì)象(渲染對(duì)象的背景柬泽、邊框等)。用這種方法可以使重繪只需查找一次樹(shù)嫁蛇,而不需要多吃查找——繪制所有的背景锨并、所有的圖片、所有的邊框等棠众。Firefox優(yōu)化了這個(gè)過(guò)程琳疏,它不會(huì)添加被隱藏的元素,比如元素完全在其他不透明元素下面闸拿。
2.7.4 Webkit 矩形存儲(chǔ)
重繪前空盼,Webkit會(huì)將舊的矩形保存為位圖,然后只重繪新舊矩形的差集新荤。
2.8 動(dòng)態(tài)變化
瀏覽器總是以盡可能小的動(dòng)作響應(yīng)一個(gè)變化揽趾,所以一個(gè)元素顏色的變化只會(huì)導(dǎo)致該元素的重繪,元素位置的變化將導(dǎo)致該元素苛骨、它的子元素和兄弟元素的重新布局和重繪篱瞎。添加一個(gè)DOM節(jié)點(diǎn),也會(huì)導(dǎo)致這個(gè)元素的布局和重繪痒芝。一些主要的變化俐筋,比如增加“html”元素的font-size
,將會(huì)導(dǎo)致緩存失效严衬,從而引起整棵樹(shù)的重新布局和重繪澄者。
2.9 渲染引擎的線程
渲染引擎是單線程的,除了網(wǎng)絡(luò)操作外诉字,幾乎所有的事情都在這個(gè)單一的線程中處理寸痢。在Firefox和Safari中梦重,這是瀏覽器的主線程蝗敢;在Chrome中,這是它tab的主線程绵脯。
網(wǎng)絡(luò)操作是由幾個(gè)并行的線程執(zhí)行谦趣,并行連接的個(gè)數(shù)是受限的(通常是2-6個(gè)連接)季研。
2.9.1 事件循環(huán)
瀏覽器的主線程是一個(gè)事件循環(huán)嫌套,它被設(shè)計(jì)為無(wú)限循環(huán)逆屡,以保持執(zhí)行過(guò)程的可用,它一直等待事件(例如layout和paint事件)并執(zhí)行它們灌危。下面是Firefox的主要事件循環(huán)代碼:
while (!mExiting)
NS_ProcessNextEvent(thread);
2.10 CSS2 可視化模型
2.10.1 畫(huà)布(The canvas)
根據(jù)CSS2規(guī)范康二,術(shù)語(yǔ)canvas是用來(lái)描述“格式化結(jié)構(gòu)所渲染的空間”——瀏覽器繪制內(nèi)容的地方碳胳。Canvas對(duì)每個(gè)維度空間都是無(wú)限大的勇蝙,但是瀏覽器基于viewport的大小選擇了一個(gè)初始寬度。根據(jù)zindex.html的定義挨约,canvas如果是位于其他canvas內(nèi)則是透明的味混,否則瀏覽器會(huì)指定一個(gè)顏色。
2.10.2 CSS 盒模型
CSS 盒模型描述了矩形盒诫惭,這些矩形盒是為了文檔樹(shù)中的元素生成的翁锡,并根據(jù)可視的格式化模型進(jìn)行布局。每個(gè)盒子包括內(nèi)容區(qū)域(如圖片夕土、文本等)及可選的四周padding馆衔、border和margin區(qū)域。
每個(gè)節(jié)點(diǎn)生成0到n個(gè)這樣的box怨绣,所有的元素都有一個(gè)display
屬性角溃,用來(lái)決定它們生成box的類型,例如:
block - 生成塊狀block
inline - 生成一個(gè)或多個(gè)行內(nèi)block
none - 不生成block
默認(rèn)是inline
篮撑,但是瀏覽器樣式設(shè)置了其他默認(rèn)值减细。例如,div元素默認(rèn)是display: block;
赢笨,你可以訪問(wèn)這里查看更多的默認(rèn)樣式表例子未蝌。
2.10.3 定位策略(Positioning scheme)
這里有三種定位策略:
- normal - 對(duì)象根據(jù)它在文檔中的位置來(lái)定位,這意味著它在渲染樹(shù)和DOM樹(shù)中的位置是一致的茧妒,并根據(jù)它的盒模型和大小進(jìn)行布局
- float - 對(duì)象先像正常的流一樣布局萧吠,然后盡可能的向左或向右移動(dòng)
- absolute - 對(duì)象在渲染樹(shù)中的位置和DOM樹(shù)中的位置無(wú)關(guān)
定位策略是通過(guò)設(shè)置position
屬性和float
屬性來(lái)實(shí)現(xiàn)的:
-
static
和relative
會(huì)導(dǎo)致normal
定位 -
absolute
和fixed
會(huì)導(dǎo)致absolute
定位
在static
定位中,不定義位置而使用默認(rèn)的位置桐筏。在其他策略中纸型,作者指定位置——top
, bottom
, left
, right
。
Box布局的方式由這幾項(xiàng)決定:
- Box類型
- Box大小
- 定位策略
- 擴(kuò)展信息(比如圖片的大小和屏幕尺寸)
2.10.4 Box 類型
Block Box:構(gòu)成一個(gè)塊,在瀏覽器窗口上有自己的矩形绊袋。
Inline Box:并沒(méi)有自己的塊狀區(qū)域毕匀,包含在一個(gè)塊狀區(qū)域內(nèi)。
block是一個(gè)挨著一個(gè)垂直格式化癌别,inline則在水平方向上格式化皂岔。
Inline boxes放置在行內(nèi) box中,每行至少和最高的box一樣高展姐,當(dāng)box以baseline
對(duì)齊時(shí)躁垛,即一個(gè)元素的底部和另一個(gè)box上除底部以外的某點(diǎn)對(duì)齊,行高可以比最高的box高圾笨。當(dāng)容器寬度不夠時(shí)教馆,行內(nèi)元素將被放到多放中,這在p元素中經(jīng)常發(fā)生擂达。
2.10.5 定位(Positioning)
2.10.5.1 Relative
相對(duì)定位土铺,先按照一般的定位,然后按所要求的差值移動(dòng)板鬓。
2.10.5.2 Floats
一個(gè)浮動(dòng)的box移動(dòng)到一行的最左邊或最右邊悲敷,其余的box圍繞在它周圍。下面這段HTML:
<p>
<x:img style="float: right;" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
將顯示為:
2.10.5.3 Absolute and fixed
這種情況下的布局完全不顧普通的文檔流俭令,元素不屬于文檔流的一部分后德,大小取決于容器。在Fixed布局中抄腔,容器就是viewport(可視區(qū)域)瓢湃。
注意:fixed元素即使文檔滾動(dòng)時(shí)也不會(huì)移動(dòng)。
2.10.6 分層表示(Layered representation)
這是有CSS屬性中的z-index
指定赫蛇,表示盒模型的第三個(gè)大小绵患,即在z軸上的位置。Box分發(fā)到堆棧中(稱為堆棧上下文)棍掐,每個(gè)堆棧中靠后的元素將較早的繪制藏雏,棧頂靠前的元素離用戶最近,當(dāng)發(fā)生交疊時(shí)作煌,將隱藏靠后的元素掘殴。堆棧根據(jù)z-index
屬性排列,擁有z-index
屬性的box形成了一個(gè)局部堆棧粟誓,viewport有外部堆棧奏寨,例如:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div style="z-index:3;background-color:red;width:1in;height:1in;"></div>
<div style="z-index:1;background-color:green;width:2in;height:2in;"></div>
</p>
雖然綠色div排在紅色div后面,可能在正常流中也已經(jīng)被繪制在后面鹰服,但z-index
有更高的優(yōu)先級(jí)病瞳,所以在根box的堆棧中更靠前揽咕。