這篇文章是以色列開發(fā)人員塔利·加希爾的研究成果。她在查閱了所有公開發(fā)布的關(guān)于瀏覽器內(nèi)部機(jī)制的數(shù)據(jù),并花了很多時(shí)間來(lái)研讀網(wǎng)絡(luò)瀏覽器的源代碼。她寫道:
在 IE 占據(jù) 90%市場(chǎng)份額的年代,我們除了把瀏覽器當(dāng)成一個(gè)“黑箱”,什么也做不了。但是現(xiàn)在,開放源代碼的瀏覽器擁有了過半的市場(chǎng)份額,因此,是時(shí)候來(lái)揭開神秘的面紗,一探網(wǎng)絡(luò)瀏覽器的內(nèi)幕了。呃,里面只有數(shù)以百萬(wàn)行計(jì)的C++ 代碼…
本篇文章的英文原版:How Browsers Work: Behind the Scenes of Modern Web Browsers。
本文主要參考和更新自下面兩篇譯文:
1)前端必讀:瀏覽器內(nèi)部工作原理
2)前端文摘:深入解析瀏覽器的幕后工作原理
作為一名網(wǎng)絡(luò)開發(fā)人員,學(xué)習(xí)瀏覽器的內(nèi)部工作原理將有助于您作出更明智的決策,并理解那些最佳開發(fā)實(shí)踐的個(gè)中緣由。盡管這是一篇相當(dāng)長(zhǎng)的文檔,但是我們建議您花些時(shí)間來(lái)仔細(xì)閱讀;讀完之后,您肯定會(huì)覺得所費(fèi)不虛。
保羅·愛麗詩(shī)(Paul Irish),Chrome 瀏覽器開發(fā)人員事務(wù)部
瀏覽器是使用最廣的軟件之一。在這篇博文中,我將介紹瀏覽器的幕后工作原理。通過閱讀本文,我們將會(huì)了解,從您在地址欄輸入 google.com ,直到您在瀏覽器屏幕上看到 Google 首頁(yè)的整個(gè)過程中都發(fā)生了些什么。
目前使用的主流瀏覽器有五個(gè):Internet Explorer、Firefox、Safari、Chrome和 Opera瀏覽器。本文主要以開源瀏覽器為主進(jìn)行分析,即 Firefox、Chrome和 Safari(部分開源)。根據(jù) StatCounter 瀏覽器統(tǒng)計(jì)數(shù)據(jù),目前(2016年 2 月)Firefox(14.67%)、Safari(9.46%)和 Chrome(55.33%) 瀏覽器的總市場(chǎng)占有率將近 80%(這個(gè)數(shù)字在2011年8月的時(shí)候,才將近60%)。由此可見,如今開源瀏覽器在瀏覽器市場(chǎng)中占據(jù)絕大多數(shù)的市場(chǎng)份額。
瀏覽器的主要功能就是向服務(wù)器發(fā)出請(qǐng)求,在瀏覽器窗口中展示您想要訪問的網(wǎng)絡(luò)資源。這里所說的資源一般是指 HTML 文檔,也可以是 PDF、圖片或其他的類型。資源的位置由用戶使用 URI(統(tǒng)一資源標(biāo)示符)指定。
瀏覽器解釋并顯示HTML文件的方式是在 HTML 和 CSS 規(guī)范中指定的。這些規(guī)范由網(wǎng)絡(luò)標(biāo)準(zhǔn)化組織 W3C(萬(wàn)維網(wǎng)聯(lián)盟)進(jìn)行維護(hù)。多年以來(lái),各瀏覽器都沒有完全遵從這些規(guī)范,同時(shí)還在開發(fā)自己獨(dú)有的擴(kuò)展程序,這給網(wǎng)絡(luò)開發(fā)人員帶來(lái)了嚴(yán)重的兼容性問題。如今,大多數(shù)的瀏覽器都開始盡量遵從規(guī)范。
瀏覽器的用戶界面有很多彼此相同的元素,其中包括:用來(lái)輸入 URI 的地址欄;前進(jìn)和后退按鈕;書簽設(shè)置選項(xiàng);用于刷新和停止加載當(dāng)前文檔的刷新和停止按鈕;用于返回主頁(yè)的主頁(yè)按鈕。
奇怪的是,瀏覽器的用戶界面并沒有任何正式的規(guī)范,這是多年來(lái)的最佳實(shí)踐自然發(fā)展以及彼此模仿的結(jié)果。HTML5 也沒有定義瀏覽器必須具有的用戶界面元素,但列出了一些通用的元素,例如地址欄、狀態(tài)欄和工具欄等。當(dāng)然,各瀏覽器也可以有自己獨(dú)特的功能,比如 Firefox 的下載管理器。
瀏覽器的主要組件包括:
數(shù)據(jù)存儲(chǔ)。這是持久層。瀏覽器需要在硬盤上保存各種數(shù)據(jù),例如 Cookie。新的 HTML 規(guī)范 (HTML5)定義了“網(wǎng)絡(luò)數(shù)據(jù)庫(kù)”,這是一個(gè)完整(但是輕便)的瀏覽器內(nèi)數(shù)據(jù)庫(kù)。
值得注意的是,不同于大多數(shù)瀏覽器,Chrome 瀏覽器為每個(gè)標(biāo)簽頁(yè)(Tab)都分配了各自的渲染引擎實(shí)例,每個(gè)標(biāo)簽頁(yè)都是一個(gè)獨(dú)立的進(jìn)程(即每個(gè)標(biāo)簽頁(yè)面都在獨(dú)立的“沙箱”內(nèi)運(yùn)行,在提高安全性的同時(shí),一個(gè)標(biāo)簽頁(yè)面的崩潰也不會(huì)導(dǎo)致其他標(biāo)簽頁(yè)面被關(guān)閉)。
對(duì)于構(gòu)成瀏覽器的這些組件,后面會(huì)逐一詳細(xì)討論。
渲染引擎的職責(zé)就是渲染,即在瀏覽器窗口中顯示所請(qǐng)求的內(nèi)容。這是每一個(gè)瀏覽器的核心部分,所以渲染引擎也稱為瀏覽器內(nèi)核。
默認(rèn)情況下,渲染引擎可顯示 HTML 和 XML 文檔及圖片。通過插件(或?yàn)g覽器擴(kuò)展程序),還瀏覽器渲染引擎也可以顯示其它類型的內(nèi)容。例如,使用 PDF 查看器插件就能顯示 PDF 文檔。在本章中,我們將集中介紹其主要用途:顯示應(yīng)用了CSS的 HTML 內(nèi)容和圖片。
本文所討論的瀏覽器(Firefox、Chrome和Safari)是基于兩種渲染引擎構(gòu)建的。Firefox 使用的是 Gecko,這是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome(28版本以前)瀏覽器使用的都是 Webkit。
2013年7月10日發(fā)布的Chrome 28 版本中,Chrome瀏覽器開始正式使用Blink內(nèi)核。所以,Webkit已經(jīng)成為了Chrome瀏覽器的前內(nèi)核。
Webkit 是一種開放源代碼渲染引擎,起初用于 Linux 平臺(tái),隨后由 Apple 公司進(jìn)行修改,從而支持蘋果機(jī)和 Windows。有關(guān)詳情,請(qǐng)參閱 webkit.org。
渲染引擎一開始會(huì)從網(wǎng)絡(luò)層獲取請(qǐng)求文檔的內(nèi)容,通常以8K分塊的方式完成。
獲取了文檔內(nèi)容之后,渲染引擎開始正式工作,其基本流程:
渲染引擎解析HTML文檔,并將文檔中的標(biāo)簽轉(zhuǎn)化為dom節(jié)點(diǎn)樹,即”內(nèi)容樹”。同時(shí),它也會(huì)解析外部CSS文件以及style標(biāo)簽中的樣式數(shù)據(jù)。這些樣式信息連同HTML中的”可見內(nèi)容”一道,被用于構(gòu)建另一棵樹——”渲染樹(Render樹)”。
渲染樹由一些帶有視覺屬性(如顏色、大小等)的矩形組成,這些矩形將按照正確的順序顯示在頻幕上。
渲染樹構(gòu)建完畢之后,將會(huì)進(jìn)入”布局”處理階段,即為每一個(gè)節(jié)點(diǎn)分配一個(gè)屏幕坐標(biāo)。再下一步就是繪制(painting),即遍歷render樹,并使用UI后端層繪制每個(gè)節(jié)點(diǎn)。
值得注意的是,這個(gè)過程是逐步完成的,為了更好的用戶體驗(yàn),渲染引擎將會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上,并不會(huì)等到所有的html都解析完成之后再去構(gòu)建和布局render樹。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容,同時(shí),可能還在通過網(wǎng)絡(luò)下載其余內(nèi)容。
主流程示例
從圖2.2 和圖2.3可以看出,雖然 Webkit 和 Gecko 使用的術(shù)語(yǔ)略有不同,但整體流程還是基本相同的。
既然解析是渲染引擎中一個(gè)非常重要的過程,我們將稍微深入的研究它。首先明白什么叫做解析(parsing)。
解析一個(gè)文檔就是指將這個(gè)文檔翻譯成一個(gè)可以讓代碼理解和使用的有意義的結(jié)構(gòu)。得到的結(jié)構(gòu)通常是一個(gè)代表了該文檔結(jié)構(gòu)的節(jié)點(diǎn)樹,通常稱之為解析樹或語(yǔ)法樹。
例如, 解析“2+3-1”這個(gè)表達(dá)式,可能返回這樣一棵樹。
解析是以文檔所遵循的語(yǔ)法規(guī)則(編寫文檔所用的語(yǔ)言或格式)為基礎(chǔ)的。所有可以解析的格式都必須對(duì)應(yīng)確定的語(yǔ)法(由詞匯和語(yǔ)法規(guī)則構(gòu)成)。這稱為與上下文無(wú)關(guān)的文法。人類語(yǔ)言并不屬于這樣的語(yǔ)言,因此無(wú)法用常規(guī)的解析技術(shù)進(jìn)行解析。
解析一般可分為兩個(gè)子過程:語(yǔ)法分析和詞法分析。
語(yǔ)法分析指對(duì)語(yǔ)言應(yīng)用語(yǔ)法規(guī)則。
詞法分析就是將輸入分解為符號(hào),符號(hào)是語(yǔ)言的詞匯表——基本有效單元的集合。對(duì)于人類語(yǔ)言來(lái)說,它相當(dāng)于我們字典中出現(xiàn)的所有單詞。
解析工作一般由兩個(gè)組件共同完成:
1)詞法分析器(有時(shí)也稱為標(biāo)記生成器),負(fù)責(zé)將輸入內(nèi)容分解成一個(gè)個(gè)有效標(biāo)記。詞法分析器知道如何將無(wú)關(guān)的字符(比如空格和換行符)分離出來(lái)。;
2)解析器負(fù)責(zé)根據(jù)語(yǔ)言的語(yǔ)法規(guī)則分析文檔的結(jié)構(gòu),從而構(gòu)建解析樹。
解析是一個(gè)迭代的過程。通常,解析器會(huì)向詞法分析器請(qǐng)求一個(gè)新標(biāo)記,并嘗試將其與某條語(yǔ)法規(guī)則進(jìn)行匹配。如果發(fā)現(xiàn)了匹配規(guī)則,解析器會(huì)將一個(gè)對(duì)應(yīng)于該標(biāo)記的節(jié)點(diǎn)添加到解析樹中,然后繼續(xù)請(qǐng)求下一個(gè)標(biāo)記。
如果沒有規(guī)則與該標(biāo)記匹配,解析器就會(huì)將標(biāo)記存儲(chǔ)到內(nèi)部,并繼續(xù)請(qǐng)求下一個(gè)標(biāo)記,直至找到可與所有內(nèi)部存儲(chǔ)的標(biāo)記匹配的規(guī)則。
如果沒有規(guī)則(即沒有找到相應(yīng)的語(yǔ)法規(guī)則),解析器就會(huì)引發(fā)一個(gè)異常。這意味著文檔無(wú)效,包含語(yǔ)法錯(cuò)誤。
很多時(shí)候,解析樹還不是最終結(jié)果。解析通常是在轉(zhuǎn)換過程中使用的,而轉(zhuǎn)換是指將輸入文檔轉(zhuǎn)換成另一種格式。編譯就是一個(gè)例子。編譯器可將源代碼編譯成機(jī)器代碼,具體過程是首先將源代碼解析成解析樹,然后將解析樹翻譯成機(jī)器代碼文檔。
在圖3.1中,我們通過一個(gè)數(shù)學(xué)表達(dá)式建立了解析樹?,F(xiàn)在,讓我們?cè)囍x一個(gè)簡(jiǎn)單的數(shù)學(xué)語(yǔ)言,用來(lái)演示解析的過程。
詞匯表:我們用的語(yǔ)言可包含整數(shù)、加號(hào)和減號(hào)。
語(yǔ)法規(guī)則:1)構(gòu)成語(yǔ)言的語(yǔ)法單位是表達(dá)式、項(xiàng)和運(yùn)算符。2)該語(yǔ)言可以包括多個(gè)表達(dá)式。3)一個(gè)表達(dá)式定義為兩個(gè)項(xiàng)通過一個(gè)操作符連接。4)運(yùn)算符可以是加號(hào)或減號(hào)。5)項(xiàng)可以是一個(gè)整數(shù)或一個(gè)表達(dá)式。
現(xiàn)在來(lái)分析一下”2+3-1”這個(gè)輸入。
匹配語(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)詞匯通常用正則表達(dá)式表示。
例如,我們的示例語(yǔ)言可以定義如下:
INTEGER :0|[1-9][0-9]*PLUS : +MINUS: -
正如您所看到的,這里用正則表達(dá)式給出了整數(shù)的定義。
2)語(yǔ)法通常使用一種稱為 BNF 的格式來(lái)定義。
我們的示例語(yǔ)言可以定義如下:
:= term operation termoperation := PLUS | MINUSterm := INTEGER |
之前我們說過,如果語(yǔ)言的語(yǔ)法是與上下文無(wú)關(guān)的語(yǔ)法,就可以由常規(guī)解析器進(jìn)行解析。
與上下文無(wú)關(guān)的語(yǔ)法的直觀定義就是可以完全用BNF格式表達(dá)的語(yǔ)法。有關(guān)正式定義,請(qǐng)參閱關(guān)于與上下文無(wú)關(guān)的語(yǔ)法的維基百科文章。
有兩種基本類型的解析器:自上而下解析器和自下而上解析器。直觀地來(lái)說,自上而下的解析器從語(yǔ)法的高層結(jié)構(gòu)出發(fā),嘗試從中找到匹配的結(jié)構(gòu)。而自下而上的解析器從低層規(guī)則出發(fā),將輸入內(nèi)容逐步轉(zhuǎn)化為語(yǔ)法規(guī)則,直至滿足高層規(guī)則。
讓我們來(lái)看看這兩種解析器會(huì)如何解析我們的示例:
自頂向下解析器從最高層規(guī)則開始——它先識(shí)別出”2+3”,將其視為一個(gè)表達(dá)式,然后識(shí)別出”2+3-1”為一個(gè)表達(dá)式(識(shí)別表達(dá)式的過程中匹配了其他規(guī)則,但起點(diǎn)是最高層規(guī)則)。
自下而上的解析器將掃描輸入內(nèi)容,找到匹配的規(guī)則后,將匹配的輸入內(nèi)容替換成規(guī)則。如此繼續(xù)替換,直到輸入內(nèi)容的結(jié)尾。部分匹配的表達(dá)式保存在解析器的堆棧中。
堆棧(Stack) | 輸入(Input) |
---|---|
2+3-1 | |
項(xiàng) | +3-1 |
項(xiàng)運(yùn)算 | 3-1 |
表達(dá)式 | -1 |
表達(dá)式運(yùn)算符 | 1 |
表達(dá)式 | - |
這種自下而上的解析器稱為移位歸約解析器,因?yàn)檩斎朐谙蛴乙莆唬ㄔO(shè)想有一個(gè)指針從輸入內(nèi)容的開頭移動(dòng)到結(jié)尾),并逐漸簡(jiǎn)化語(yǔ)法規(guī)則。
解析器生成器這個(gè)工具可以自動(dòng)生成解析器,只需要指定語(yǔ)言的文法———詞匯表及語(yǔ)法規(guī)則,它就可以生成一個(gè)解析器。創(chuàng)建一個(gè)解析器需要對(duì)解析有深入的理解,而且手動(dòng)的創(chuàng)建一個(gè)有較好性能的解析器并不容易,所以解析生成器很有用。
Webkit使用兩個(gè)知名的解析生成器——用于創(chuàng)建語(yǔ)法分析器的Flex及創(chuàng)建解析器的Bison(你可能接觸過Lex和Yacc)。Flex的輸入是一個(gè)包含了符號(hào)定義的正則表達(dá)式,Bison的輸入是用BNF格式表示的語(yǔ)法規(guī)則。
HTML 解析器的任務(wù)是將 HTML 標(biāo)記解析成解析樹。
W3C組織制定規(guī)范定義了HTML的詞匯表和語(yǔ)法。
正如在解析簡(jiǎn)介中提到的,上下文無(wú)關(guān)文法的語(yǔ)法可以用類似BNF的格式來(lái)定義。
很遺憾,所有的常規(guī)解析器都不適用于 HTML(我并不是開玩笑,它們可以用于解析 CSS 和 JavaScript)。HTML 并不能用解析器所需的與上下文無(wú)關(guān)的語(yǔ)法來(lái)定義。
Html有一個(gè)正式的格式定義:DTD(Document Type Definition,文檔類型定義),但它并不是上下文無(wú)關(guān)的語(yǔ)法。
這初看起來(lái)很奇怪:HTML 和 XML 非常相似。有很多 XML 解析器可以使用。HTML 存在一個(gè) XML 變體 (XHTML),那么有什么大的區(qū)別呢?區(qū)別在于 HTML 的處理更為“寬容”,它允許您省略某些隱式添加的標(biāo)記,有時(shí)還能省略一些起始或者結(jié)束標(biāo)記等等。和 XML 嚴(yán)格的語(yǔ)法不同,HTML 整體來(lái)看是一種“軟性”的語(yǔ)法。
顯然,這種看上去細(xì)微的差別實(shí)際上卻帶來(lái)了巨大的影響。一方面,這是 HTML 如此流行的原因:它能包容您的錯(cuò)誤,簡(jiǎn)化網(wǎng)絡(luò)開發(fā)。另一方面,這使得它很難編寫正式的語(yǔ)法。概括地說,HTML 無(wú)法很容易地通過常規(guī)解析器解析(因?yàn)樗恼Z(yǔ)法不是與上下文無(wú)關(guān)的語(yǔ)法),也無(wú)法通過 XML 解析器來(lái)解析。
HTML的定義采用了DTD格式。此格式適用于定義SGML族的語(yǔ)言。它包括所有允許使用的元素及其屬性和層次結(jié)構(gòu)的定義。如上文所述,HTML DTD無(wú)法構(gòu)成與上下文無(wú)關(guān)的語(yǔ)法。
DTD存在一些變體。嚴(yán)格模式完全遵守HTML規(guī)范,而其他模式可支持以前的瀏覽器所使用的標(biāo)記。這樣做的目的是確保向下兼容一些早期版本的內(nèi)容。最新的嚴(yán)格模式DTD可以在這里找到:www.w3.org/TR/html4/strict.dtd
解析器的輸出(即”解析樹”)是由DOM元素及屬性節(jié)點(diǎn)組成的。DOM是文檔對(duì)象模型(Document Object Model) 的縮寫。它是HTML文檔的對(duì)象表示,同時(shí)也是外部?jī)?nèi)容(例如 JavaScript)與HTML元素之間的接口。
解析樹的根節(jié)點(diǎn)是”Document”對(duì)象。DOM與標(biāo)記之間幾乎是一一對(duì)應(yīng)的關(guān)系。比如下面這段標(biāo)記:
<html> <body> <p> Hello World </p> <div> <img src="example.png"/></div> </body></html>
可翻譯成如下的 DOM 樹:
這里所說的DOM節(jié)點(diǎn)樹,指的是那些實(shí)現(xiàn)了DOM接口的元素組成的樹。
我們?cè)谥罢鹿?jié)已經(jīng)說過,HTML無(wú)法用常規(guī)的自上而下或自下而上的解析器進(jìn)行解析。原因在于:
語(yǔ)言本身的寬容特性;
瀏覽器對(duì)一些常見的非法html有容錯(cuò)機(jī)制;
解析過程需要不斷地反復(fù)。源內(nèi)容在解析過程中通常不會(huì)改變,但是在HTML中,腳本標(biāo)記如果包含 “document.write”,就會(huì)添加額外的標(biāo)記,這樣解析過程實(shí)際上就更改了輸入內(nèi)容。
由于不能使用常規(guī)的解析技術(shù),瀏覽器為html定制了專屬的解析器。
HTML5規(guī)范詳細(xì)地描述了解析算法。此算法由兩個(gè)階段組成:符號(hào)化及構(gòu)建樹。
符號(hào)化是詞法分析的過程,將輸入內(nèi)容解析成多個(gè)標(biāo)記,HTML標(biāo)記包括起始標(biāo)記、結(jié)束標(biāo)記、屬性名稱和屬性值。標(biāo)記生成器識(shí)別標(biāo)記,傳遞給樹構(gòu)造器,然后讀取下一個(gè)字符以識(shí)別下一個(gè)標(biāo)記,如此反復(fù)直到輸入的結(jié)束。
該算法的輸出結(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)和樹結(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ú)法在此詳述,所以我們通過一個(gè)簡(jiǎn)單的示例來(lái)幫助大家理解其原理。
基本示例 - 將下面的 HTML 代碼標(biāo)記化:
<html> <body> Hello world </body></html>
初始狀態(tài)是數(shù)據(jù)狀態(tài)。遇到字符 <
時(shí),狀態(tài)更改為“標(biāo)記打開狀態(tài)”。接收一個(gè) a-z字符會(huì)創(chuàng)建“起始標(biāo)記”,狀態(tài)更改為“標(biāo)記名稱狀態(tài)”。這個(gè)狀態(tài)會(huì)一直保持到接收>
字符。在此期間接收的每個(gè)字符都會(huì)附加到新的標(biāo)記名稱上。在本例中,我們創(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)記打開狀態(tài)”。接收下一個(gè)輸入字符 /
時(shí),會(huì)創(chuàng)建 end tag token 并改為“標(biāo)記名稱狀態(tài)”。我們會(huì)再次保持這個(gè)狀態(tài),直到接收 >
。然后將發(fā)送新的標(biāo)記,并回到“數(shù)據(jù)狀態(tài)”。</html>
輸入也會(huì)進(jìn)行同樣的處理。
在創(chuàng)建解析器的同時(shí),也會(huì)創(chuàng)建 Document 對(duì)象。在樹構(gòu)建階段,以 Document 為根節(jié)點(diǎn)的 DOM 樹也會(huì)不斷進(jìn)行修改,向其中添加各種元素。標(biāo)記生成器發(fā)送的每個(gè)節(jié)點(diǎn)都會(huì)由樹構(gòu)建器進(jìn)行處理。規(guī)范中定義了每個(gè)標(biāo)記所對(duì)應(yīng)的 DOM 元素,這些元素會(huì)在接收到相應(yīng)的標(biāo)記時(shí)創(chuàng)建。這些元素不僅會(huì)添加到 DOM 樹中,還會(huì)添加到開放元素的堆棧中。此堆棧用于糾正嵌套錯(cuò)誤和處理未關(guān)閉的標(biāo)記。其算法也可以用狀態(tài)機(jī)來(lái)描述。這些狀態(tài)稱為“插入模式”。
讓我們來(lái)看看示例輸入的樹構(gòu)建過程:
<html> <body> Hello world </body></html>
樹構(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)記。即使我們的示例中沒有“head”標(biāo)記,系統(tǒng)也會(huì)隱式創(chuàng)建一個(gè) HTMLHeadElement,并將其添加到樹中。
現(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)椤癰ody”。
現(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”模式?,F(xiàn)在我們將接收 HTML 結(jié)束標(biāo)記,然后進(jìn)入“after after body”模式。接收到文件結(jié)束標(biāo)記后,解析過程就此結(jié)束。
在此階段,瀏覽器會(huì)將文檔標(biāo)注為交互狀態(tài),并開始解析那些處于“deferred”模式的腳本,也就是那些應(yīng)在文檔解析完成后才執(zhí)行的腳本。然后,文檔狀態(tài)將設(shè)置為“完成”,一個(gè)“加載”事件將隨之觸發(fā)。您可以在 HTML5 規(guī)范中查看標(biāo)記化和樹構(gòu)建的完整算法
您在瀏覽 HTML 網(wǎng)頁(yè)時(shí)從來(lái)不會(huì)看到“語(yǔ)法無(wú)效”的錯(cuò)誤。這是因?yàn)闉g覽器會(huì)糾正任何無(wú)效內(nèi)容,然后繼續(xù)工作。
以下面的 HTML 代碼為例:
<html> <mytag> </mytag> <div> <p> </div> Really lousy HTML </p></html>
在這里,我已經(jīng)違反了很多語(yǔ)法規(guī)則(“mytag”不是標(biāo)準(zhǔn)的標(biāo)記,“p”和“div”元素之間的嵌套有誤等等),但是瀏覽器仍然會(huì)正確地顯示這些內(nèi)容,并且毫無(wú)怨言。因?yàn)橛写罅康慕馕銎鞔a會(huì)糾正 HTML 網(wǎng)頁(yè)作者的錯(cuò)誤。
不同瀏覽器的錯(cuò)誤處理機(jī)制相當(dāng)一致,但令人稱奇的是,這種機(jī)制并不是 HTML 當(dāng)前規(guī)范的一部分。和書簽管理以及前進(jìn)/后退按鈕一樣,它也是瀏覽器在多年發(fā)展中的產(chǎn)物。很多網(wǎng)站都普遍存在著一些已知的無(wú)效 HTML 結(jié)構(gòu),每一種瀏覽器都會(huì)嘗試通過和其他瀏覽器一樣的方式來(lái)修復(fù)這些無(wú)效結(jié)構(gòu)。
HTML5 規(guī)范定義了一部分這樣的要求。Webkit 在 HTML 解析器類的開頭注釋中對(duì)此做了很好的概括。
解析器對(duì)標(biāo)記化輸入內(nèi)容進(jìn)行解析,以構(gòu)建文檔樹。如果文檔的格式正確,就直接進(jìn)行解析。
遺憾的是,我們不得不處理很多格式錯(cuò)誤的 HTML 文檔,所以解析器必須具備一定的容錯(cuò)性。
我們至少要能夠處理以下錯(cuò)誤情況:
明顯不能在某些外部標(biāo)記中添加的元素。在此情況下,我們應(yīng)該關(guān)閉所有標(biāo)記,直到出現(xiàn)禁止添加的元素,然后再加入該元素。
我們不能直接添加的元素。這很可能是網(wǎng)頁(yè)作者忘記添加了其中的一些標(biāo)記(或者其中的標(biāo)記是可選的)。這些標(biāo)簽可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
向 inline 元素內(nèi)添加 block 元素。關(guān)閉所有 inline 元素,直到出現(xiàn)下一個(gè)較高級(jí)的 block 元素。
如果這樣仍然無(wú)效,可關(guān)閉所有元素,直到可以添加元素為止,或者忽略該標(biāo)記。
讓我們看一些 Webkit 容錯(cuò)的示例:
使用了 </br>
而不是 <br>
有些網(wǎng)站使用了 </br>
而不是 <br>
。為了與 IE 和 Firefox 兼容,Webkit 將其與 <br>
做同樣的處理。
代碼如下:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true;}<span class="kwd"> </span>
請(qǐng)注意,錯(cuò)誤處理是在內(nèi)部進(jìn)行的,用戶并不會(huì)看到這個(gè)過程。
離散表格
離散表格是指位于其他表格內(nèi)容中,但又不在任何一個(gè)單元格內(nèi)的表格。
比如以下的示例:
<table> <table> <tr><td>inner table</td></tr> </table> <tr><td>outer table</td></tr></table>
Webkit 會(huì)將其層次結(jié)構(gòu)更改為兩個(gè)同級(jí)表格:
<table> <tr><td>outer table</td></tr></table><table> <tr><td>inner table</td></tr></table>
代碼如下:
if (m_inStrayTableContent && localName == tableTag) popBlock(tableTag);
Webkit 使用一個(gè)堆棧來(lái)保存當(dāng)前的元素內(nèi)容,它會(huì)從外部表格的堆棧中彈出內(nèi)部表格?,F(xiàn)在,這兩個(gè)表格就變成了同級(jí)關(guān)系。
嵌套的表單元素
如果用戶在一個(gè)表單元素中又放入了另一個(gè)表單,那么第二個(gè)表單將被忽略。
代碼如下:
if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document);}<span class="kwd"> </span>
過于復(fù)雜的標(biāo)記層次結(jié)構(gòu)
代碼的注釋已經(jīng)說得很清楚了。
示例網(wǎng)站 www.liceo.edu.mx 嵌套了約 1500 個(gè)標(biāo)記,全都來(lái)自一堆 <b>
標(biāo)記。我們只允許最多 20 層同類型標(biāo)記的嵌套,如果再嵌套更多,就會(huì)全部忽略。
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)記。
同樣,代碼的注釋已經(jīng)說得很清楚了。支持格式非常糟糕的 HTML 代碼。我們從不關(guān)閉 body 標(biāo)記,因?yàn)橐恍┯薮赖木W(wǎng)頁(yè)會(huì)在實(shí)際文檔結(jié)束之前就關(guān)閉。我們通過調(diào)用 end() 來(lái)執(zhí)行關(guān)閉操作。
if (t->tagName == htmlTag || t->tagName == bodyTag ) return;
所以網(wǎng)頁(yè)作者需要注意,除非您想作為反面教材出現(xiàn)在 Webkit 容錯(cuò)代碼段的示例中,否則還請(qǐng)編寫格式正確的 HTML 代碼。
還記得簡(jiǎn)介中解析的概念嗎?和HTML不同,CSS是上下文無(wú)關(guān)的語(yǔ)法,可以使用簡(jiǎn)介中描述的各種解析器進(jìn)行解析。事實(shí)上,CSS 規(guī)范定義了 CSS 的詞法和語(yǔ)法。讓我們來(lái)看一些示例:
詞法(詞匯)是針對(duì)各個(gè)標(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)識(shí)符 (identifier) 的縮寫,比如類名。”name”是元素的 ID(通過”#”來(lái)引用)。
語(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*] ')' ] ;<span class="kwd"> </span>
解釋:這是一個(gè)規(guī)則集的結(jié)構(gòu):
div.error , a.error { color:red; font-weight:bold;}
div.error 和 a.error 是選擇器。大括號(hào)內(nèi)的部分包含了由此規(guī)則集應(yīng)用的規(guī)則。此結(jié)構(gòu)的正式定義是這樣的:
ruleset : selector [ ',' S* selector ]* '{' S* declaration [ ';' S* declaration ]* '}' S* ;
這表示一個(gè)規(guī)則集就是一個(gè)選擇器,或者由逗號(hào)和空格(S 表示空格)分隔的多個(gè)(數(shù)量可選)選擇器。規(guī)則集包含了大括號(hào),以及其中的一個(gè)或多個(gè)(數(shù)量可選)由分號(hào)分隔的聲明?!奥暶鳌焙汀斑x擇器”將由下面的 BNF 格式定義。
Webkit 使用 Flex 和 Bison 解析器生成器,通過 CSS 語(yǔ)法文件自動(dòng)創(chuàng)建解析器。正如我們之前在解析器簡(jiǎn)介中所說,Bison 會(huì)創(chuàng)建自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。這兩種解析器都會(huì)將 CSS 文件解析成 StyleSheet 對(duì)象,且每個(gè)對(duì)象都包含 CSS 規(guī)則。CSS 規(guī)則對(duì)象則包含選擇器和聲明對(duì)象,以及其他與 CSS 語(yǔ)法對(duì)應(yīng)的對(duì)象。
網(wǎng)絡(luò)的模型是同步的。網(wǎng)頁(yè)作者希望解析器遇到 <script>
標(biāo)記時(shí)立即解析并執(zhí)行腳本。文檔的解析將停止,直到腳本執(zhí)行完畢。如果腳本是外部的,那么解析過程會(huì)停止,直到從網(wǎng)絡(luò)同步抓取資源完成后再繼續(xù)。此模型已經(jīng)使用了多年,也在 HTML4 和 HTML5 規(guī)范中進(jìn)行了指定。作者也可以將腳本標(biāo)注為“defer”,這樣它就不會(huì)停止文檔解析,而是等到解析結(jié)束才執(zhí)行。HTML5 增加了一個(gè)選項(xiàng),可將腳本標(biāo)記為異步,以便由其他線程解析和執(zhí)行。
Webkit 和 Firefox 都進(jìn)行了這項(xiàng)優(yōu)化。在執(zhí)行腳本時(shí),其他線程會(huì)解析文檔的其余部分,找出并加載需要通過網(wǎng)絡(luò)加載的其他資源。通過這種方式,資源可以在并行連接上加載,從而提高總體速度。請(qǐng)注意,預(yù)解析器不會(huì)修改 DOM 樹,而是將這項(xiàng)工作交由主解析器處理;預(yù)解析器只會(huì)解析外部資源(例如外部腳本、樣式表和圖片)的引用。
另一方面,樣式表有著不同的模型。理論上來(lái)說,應(yīng)用樣式表不會(huì)更改 DOM 樹,因此似乎沒有必要等待樣式表并停止文檔解析。但這涉及到一個(gè)問題,就是腳本在文檔解析階段會(huì)請(qǐng)求樣式信息。如果當(dāng)時(shí)還沒有加載和解析樣式,腳本就會(huì)獲得錯(cuò)誤的回復(fù),這樣顯然會(huì)產(chǎn)生很多問題。這看上去是一個(gè)非典型案例,但事實(shí)上非常普遍。Firefox 在樣式表加載和解析的過程中,會(huì)禁止所有腳本。而對(duì)于 Webkit 而言,僅當(dāng)腳本嘗試訪問的樣式屬性可能受尚未加載的樣式表影響時(shí),它才會(huì)禁止該腳本。
在 DOM 樹構(gòu)建的同時(shí),瀏覽器還會(huì)構(gòu)建另一個(gè)樹結(jié)構(gòu):渲染樹。這是由可視化元素按照其顯示順序而組成的樹,也是文檔的可視化表示。它的作用是讓您按照正確的順序繪制內(nèi)容。
Firefox 將渲染樹中的元素稱為“框架”。Webkit 使用的術(shù)語(yǔ)是呈現(xiàn)器或呈現(xiàn)對(duì)象。
呈現(xiàn)器知道如何布局并將自身及其子元素繪制出來(lái)。
Webkits RenderObject 類是所有呈現(xiàn)器的基類,其定義如下:
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è)呈現(xiàn)器都代表了一個(gè)矩形的區(qū)域,通常對(duì)應(yīng)于相關(guān)節(jié)點(diǎn)的 CSS 框,這一點(diǎn)在 CSS2 規(guī)范中有所描述。它包含諸如寬度、高度和位置等幾何信息。
框的類型會(huì)受到與節(jié)點(diǎn)相關(guān)的“display”樣式屬性的影響(請(qǐng)參閱樣式計(jì)算章節(jié))。下面這段 Webkit 代碼描述了根據(jù) display 屬性的不同,針對(duì)同一個(gè) DOM 節(jié)點(diǎn)應(yīng)創(chuàng)建什么類型的呈現(xiàn)器。
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;}
元素類型也是考慮因素之一,例如表單控件和表格都對(duì)應(yīng)特殊的框架。
在 Webkit 中,如果一個(gè)元素需要?jiǎng)?chuàng)建特殊的呈現(xiàn)器,就會(huì)替換 createRenderer方法。呈現(xiàn)器所指向的樣式對(duì)象中包含了一些和幾何無(wú)關(guān)的信息。
呈現(xiàn)器是和 DOM 元素相對(duì)應(yīng)的,但并非一一對(duì)應(yīng)。非可視化的 DOM 元素不會(huì)插入渲染樹中,例如“head”元素。如果元素的 display 屬性值為“none”,那么也不會(huì)顯示在渲染樹中(但是 visibility 屬性值為“hidden”的元素仍會(huì)顯示)。
有一些 DOM 元素對(duì)應(yīng)多個(gè)可視化對(duì)象。它們往往是具有復(fù)雜結(jié)構(gòu)的元素,無(wú)法用單一的矩形來(lái)描述。例如,“select”元素有 3 個(gè)呈現(xiàn)器:一個(gè)用于顯示區(qū)域,一個(gè)用于下拉列表框,還有一個(gè)用于按鈕。如果由于寬度不夠,文本無(wú)法在一行中顯示而分為多行,那么新的行也會(huì)作為新的呈現(xiàn)器而添加。
另一個(gè)關(guān)于多呈現(xiàn)器的例子是格式無(wú)效的 HTML。根據(jù) CSS 規(guī)范,inline 元素只能包含 block 元素或 inline 元素中的一種。如果出現(xiàn)了混合內(nèi)容,則應(yīng)創(chuàng)建匿名的 block 呈現(xiàn)器,以包裹 inline 元素。
有一些呈現(xiàn)對(duì)象對(duì)應(yīng)于 DOM 節(jié)點(diǎn),但在樹中所在的位置與 DOM 節(jié)點(diǎn)不同。浮動(dòng)定位和絕對(duì)定位的元素就是這樣,它們處于正常的流程之外,放置在樹中的其他地方,并映射到真正的框架,而放在原位的是占位框架。
初始容器 block 為“viewport”,而在 Webkit 中則為“RenderView”對(duì)象。
在 Firefox 中,系統(tǒng)會(huì)針對(duì) DOM 更新注冊(cè)展示層,作為偵聽器。展示層將框架創(chuàng)建工作委托給 FrameConstructor,由該構(gòu)造器解析樣式(請(qǐng)參閱樣式計(jì)算)并創(chuàng)建框架。
在 Webkit 中,解析樣式和創(chuàng)建呈現(xiàn)器的過程稱為“附加”。每個(gè) DOM 節(jié)點(diǎn)都有一個(gè)“attach”方法。附加是同步進(jìn)行的,將節(jié)點(diǎn)插入 DOM 樹需要調(diào)用新的節(jié)點(diǎn)“attach”方法。
處理 html 和 body 標(biāo)記就會(huì)構(gòu)建渲染樹根節(jié)點(diǎn)。這個(gè)根節(jié)點(diǎn)呈現(xiàn)對(duì)象對(duì)應(yīng)于 CSS 規(guī)范中所說的容器 block,這是最上層的 block,包含了其他所有 block。它的尺寸就是視口,即瀏覽器窗口顯示區(qū)域的尺寸。Firefox 稱之為 ViewPortFrame,而 Webkit 稱之為 RenderView。這就是文檔所指向的呈現(xiàn)對(duì)象。渲染樹的其余部分以 DOM 樹節(jié)點(diǎn)插入的形式來(lái)構(gòu)建。
請(qǐng)參閱關(guān)于處理模型的 CSS2 規(guī)范。
構(gòu)建渲染樹時(shí),需要計(jì)算每一個(gè)呈現(xiàn)對(duì)象的可視化屬性。這是通過計(jì)算每個(gè)元素的樣式屬性來(lái)完成的。
樣式包括來(lái)自各種來(lái)源的樣式表、inline 樣式元素和 HTML 中的可視化屬性(例如“bgcolor”屬性)。其中后者將經(jīng)過轉(zhuǎn)化以匹配 CSS 樣式屬性。
樣式表的來(lái)源包括瀏覽器的默認(rèn)樣式表、由網(wǎng)頁(yè)作者提供的樣式表以及由瀏覽器用戶提供的用戶樣式表(瀏覽器允許您定義自己喜歡的樣式。以 Firefox 為例,用戶可以將自己喜歡的樣式表放在“Firefox Profile”文件夾下)。
樣式計(jì)算存在以下難點(diǎn):
樣式數(shù)據(jù)是一個(gè)超大的結(jié)構(gòu),存儲(chǔ)了無(wú)數(shù)的樣式屬性,這可能造成內(nèi)存問題。
如果不進(jìn)行優(yōu)化,為每一個(gè)元素查找匹配的規(guī)則會(huì)造成性能問題。要為每一個(gè)元素遍歷整個(gè)規(guī)則列表來(lái)尋找匹配規(guī)則,這是一項(xiàng)浩大的工程。選擇器會(huì)具有很復(fù)雜的結(jié)構(gòu),這就會(huì)導(dǎo)致某個(gè)匹配過程一開始看起來(lái)很可能是正確的,但最終發(fā)現(xiàn)其實(shí)是徒勞的,必須嘗試其他匹配路徑。
例如下面這個(gè)組合選擇器:
div div div div{…}
這意味著規(guī)則適用于作為 3 個(gè) div 元素的子代的
Webkit 節(jié)點(diǎn)會(huì)引用樣式對(duì)象 (RenderStyle)。這些對(duì)象在某些情況下可以由不同節(jié)點(diǎn)共享。這些節(jié)點(diǎn)是同級(jí)關(guān)系,并且:
這些元素必須處于相同的鼠標(biāo)狀態(tài)(例如,不允許其中一個(gè)是“:hover”狀態(tài),而另一個(gè)不是)
任何元素都沒有 ID
標(biāo)記名稱應(yīng)匹配
類屬性應(yīng)匹配
映射屬性的集合必須是完全相同的
鏈接狀態(tài)必須匹配
焦點(diǎn)狀態(tài)必須匹配
任何元素都不應(yīng)受屬性選擇器的影響,這里所說的“影響”是指在選擇器中的任何位置有任何使用了屬性選擇器的選擇器匹配
元素中不能有任何 inline 樣式屬性
不能使用任何同級(jí)選擇器。WebCore 在遇到任何同級(jí)選擇器時(shí),只會(huì)引發(fā)一個(gè)全局開關(guān),并停用整個(gè)文檔的樣式共享(如果存在)。這包括 + 選擇器以及 :first-child 和 :last-child 等選擇器。
為了簡(jiǎn)化樣式計(jì)算,F(xiàn)irefox 還采用了另外兩種樹:規(guī)則樹和樣式上下文樹。Webkit 也有樣式對(duì)象,但它們不是保存在類似樣式上下文樹這樣的樹結(jié)構(gòu)中,只是由 DOM 節(jié)點(diǎn)指向此類對(duì)象的相關(guān)樣式。
樣式上下文包含端值。要計(jì)算出這些值,應(yīng)按照正確順序應(yīng)用所有的匹配規(guī)則,并將其從邏輯值轉(zhuǎn)化為具體的值。例如,如果邏輯值是屏幕大小的百分比,則需要換算成絕對(duì)的單位。規(guī)則樹的點(diǎn)子真的很巧妙,它使得節(jié)點(diǎn)之間可以共享這些值,以避免重復(fù)計(jì)算,還可以節(jié)約空間。
所有匹配的規(guī)則都存儲(chǔ)在樹中。路徑中的底層節(jié)點(diǎn)擁有較高的優(yōu)先級(jí)。規(guī)則樹包含了所有已知規(guī)則匹配的路徑。規(guī)則的存儲(chǔ)是延遲進(jìn)行的。規(guī)則樹不會(huì)在開始的時(shí)候就為所有的節(jié)點(diǎn)進(jìn)行計(jì)算,而是只有當(dāng)某個(gè)節(jié)點(diǎn)樣式需要進(jìn)行計(jì)算時(shí),才會(huì)向規(guī)則樹添加計(jì)算的路徑。
這個(gè)想法相當(dāng)于將規(guī)則樹路徑視為詞典中的單詞。如果我們已經(jīng)計(jì)算出如下的規(guī)則樹:
假設(shè)我們需要為內(nèi)容樹中的另一個(gè)元素匹配規(guī)則,并且找到匹配路徑是 B - E - I(按照此順序)。由于我們?cè)跇渲幸呀?jīng)計(jì)算出了路徑 A - B - E - I - L,因此就已經(jīng)有了此路徑,這就減少了現(xiàn)在所需的工作量。
讓我們看看規(guī)則樹如何幫助我們減少工作。
1)結(jié)構(gòu)劃分
樣式上下文可分割成多個(gè)結(jié)構(gòu)。這些結(jié)構(gòu)體包含了特定類別(如 border 或 color)的樣式信息。結(jié)構(gòu)中的屬性都是繼承的或非繼承的。繼承屬性如果未由元素定義,則繼承自其父代。非繼承屬性(也稱為“重置”屬性)如果未進(jìn)行定義,則使用默認(rèn)值。
規(guī)則樹通過緩存整個(gè)結(jié)構(gòu)(包含計(jì)算出的端值)為我們提供幫助。這一想法假定底層節(jié)點(diǎn)沒有提供結(jié)構(gòu)的定義,則可使用上層節(jié)點(diǎn)中的緩存結(jié)構(gòu)。
2)使用規(guī)則樹計(jì)算樣式上下文
在計(jì)算某個(gè)特定元素的樣式上下文時(shí),我們首先計(jì)算規(guī)則樹中的對(duì)應(yīng)路徑,或者使用現(xiàn)有的路徑。然后我們沿此路徑應(yīng)用規(guī)則,在新的樣式上下文中填充結(jié)構(gòu)。我們從路徑中擁有最高優(yōu)先級(jí)的底層節(jié)點(diǎn)(通常也是最特殊的選擇器)開始,并向上遍歷規(guī)則樹,直到結(jié)構(gòu)填充完畢。如果該規(guī)則節(jié)點(diǎn)對(duì)于此結(jié)構(gòu)沒有任何規(guī)范,那么我們可以實(shí)現(xiàn)更好的優(yōu)化:尋找路徑更上層的節(jié)點(diǎn),找到后指定完整的規(guī)范并指向相關(guān)節(jié)點(diǎn)即可。這是最好的優(yōu)化方法,因?yàn)檎麄€(gè)結(jié)構(gòu)都能共享。這可以減少端值的計(jì)算量并節(jié)約內(nèi)存。
如果我們找到了部分定義,就會(huì)向上遍歷規(guī)則樹,直到結(jié)構(gòu)填充完畢。
如果我們找不到結(jié)構(gòu)的任何定義,那么假如該結(jié)構(gòu)是“繼承”類型,我們會(huì)在上下文樹中指向父代的結(jié)構(gòu),這樣也可以共享結(jié)構(gòu)。如果是 reset 類型的結(jié)構(gòu),則會(huì)使用默認(rèn)值。
如果最特殊的節(jié)點(diǎn)確實(shí)添加了值,那么我們需要另外進(jìn)行一些計(jì)算,以便將這些值轉(zhuǎn)化成實(shí)際值。然后我們將結(jié)果緩存在樹節(jié)點(diǎn)中,供子代使用。
如果某個(gè)元素與其同級(jí)元素都指向同一個(gè)樹節(jié)點(diǎn),那么它們就可以共享整個(gè)樣式上下文。
讓我們來(lái)看一個(gè)例子,假設(shè)我們有如下 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">another error</div> </body></html>
還有如下規(guī)則:
div {margin:5px;color:black}.err {color:red}.big {margin-top:3px}div span {margin-bottom:4px}#div1 {color:blue}#div2 {color:green}
為了簡(jiǎn)便起見,我們只需要填充兩個(gè)結(jié)構(gòu):color 結(jié)構(gòu)和 margin 結(jié)構(gòu)。color 結(jié)構(gòu)只包含一個(gè)成員(即“color”),而 margin 結(jié)構(gòu)包含四條邊。
形成的規(guī)則樹如下圖所示(節(jié)點(diǎn)的標(biāo)記方式為“節(jié)點(diǎn)名 : 指向的規(guī)則序號(hào)”):
上下文樹如下圖所示(節(jié)點(diǎn)名 : 指向的規(guī)則節(jié)點(diǎn)):
假設(shè)我們解析 HTML 時(shí)遇到了第二個(gè)
現(xiàn)在我們需要填充樣式結(jié)構(gòu)。首先要填充的是 margin 結(jié)構(gòu)。由于最后的規(guī)則節(jié)點(diǎn) (F) 并沒有添加到 margin 結(jié)構(gòu),我們需要上溯規(guī)則樹,直至找到在先前節(jié)點(diǎn)插入中計(jì)算過的緩存結(jié)構(gòu),然后使用該結(jié)構(gòu)。我們會(huì)在指定 margin 規(guī)則的最上層節(jié)點(diǎn)(即 B 節(jié)點(diǎn))上找到該結(jié)構(gòu)。
我們已經(jīng)有了 color 結(jié)構(gòu)的定義,因此不能使用緩存的結(jié)構(gòu)。由于 color 有一個(gè)屬性,我們無(wú)需上溯規(guī)則樹以填充其他屬性。我們將計(jì)算端值(將字符串轉(zhuǎn)化為 RGB 等)并在此節(jié)點(diǎn)上緩存經(jīng)過計(jì)算的結(jié)構(gòu)。
第二個(gè) 元素處理起來(lái)更加簡(jiǎn)單。我們將匹配規(guī)則,最終發(fā)現(xiàn)它和之前的 span 一樣指向規(guī)則 G。由于我們找到了指向同一節(jié)點(diǎn)的同級(jí),就可以共享整個(gè)樣式上下文了,只需指向之前 span 的上下文即可。
對(duì)于包含了繼承自父代的規(guī)則的結(jié)構(gòu),緩存是在上下文樹中進(jìn)行的(事實(shí)上 color 屬性是繼承的,但是 Firefox 將其視為 reset 屬性,并緩存到規(guī)則樹上)。
例如,如果我們?cè)谀硞€(gè)段落中添加 font 規(guī)則:
p {font-family:Verdana;font size:10px;font-weight:bold}
那么,該段落元素作為上下文樹中的 div 的子代,就會(huì)共享與其父代相同的 font 結(jié)構(gòu)(前提是該段落沒有指定 font 規(guī)則)。
在 Webkit 中沒有規(guī)則樹,因此會(huì)對(duì)匹配的聲明遍歷 4 次。首先應(yīng)用非重要高優(yōu)先級(jí)的屬性(由于作為其他屬性的依據(jù)而應(yīng)首先應(yīng)用的屬性,例如 display),接著是高優(yōu)先級(jí)重要規(guī)則,然后是普通優(yōu)先級(jí)非重要規(guī)則,最后是普通優(yōu)先級(jí)重要規(guī)則。這意味著多次出現(xiàn)的屬性會(huì)根據(jù)正確的層疊順序進(jìn)行解析。最后出現(xiàn)的最終生效。
因此概括來(lái)說,共享樣式對(duì)象(整個(gè)對(duì)象或者對(duì)象中的部分結(jié)構(gòu))可以解決問題 1和問題 3。Firefox 規(guī)則樹還有助于按照正確的順序應(yīng)用屬性。
樣式規(guī)則有一些來(lái)源:
外部樣式表或樣式元素中的 CSS 規(guī)則
p {color:blue}inline 樣式屬性及類似內(nèi)容<pstyle="color:blue"/>HTML 可視化屬性(映射到相關(guān)的樣式規(guī)則)<pbgcolor="blue"/>
后兩種很容易和元素進(jìn)行匹配,因?yàn)樵負(fù)碛袠邮綄傩?,而?HTML 屬性可以使用元素作為鍵值進(jìn)行映射。
我們之前在第 2 個(gè)問題中提到過,CSS 規(guī)則匹配可能比較棘手。為了解決這一難題,可以對(duì) CSS 規(guī)則進(jìn)行一些處理,以便訪問。
樣式表解析完畢后,系統(tǒng)會(huì)根據(jù)選擇器將 CSS 規(guī)則添加到某個(gè)哈希表中。這些哈希表的選擇器各不相同,包括 ID、類名稱、標(biāo)記名稱等,還有一種通用哈希表,適合不屬于上述類別的規(guī)則。如果選擇器是 ID,規(guī)則就會(huì)添加到 ID 表中;如果選擇器是類,規(guī)則就會(huì)添加到類表中,依此類推。
這種處理可以大大簡(jiǎn)化規(guī)則匹配。我們無(wú)需查看每一條聲明,只要從哈希表中提取元素的相關(guān)規(guī)則即可。這種優(yōu)化方法可排除掉 95% 以上規(guī)則,因此在匹配過程中根本就不用考慮這些規(guī)則了 (4.1)。
我們以如下的樣式規(guī)則為例:
p.error {color:red}#messageDiv {height:50px}div {margin:5px}
第一條規(guī)則將插入類表,第二條將插入 ID 表,而第三條將插入標(biāo)記表。
對(duì)于下面的 HTML 代碼段:
<p class="error">an error occurred </p><div id=" messageDiv">this is a message</div>
我們首先會(huì)為 p 元素尋找匹配的規(guī)則。類表中有一個(gè)“error”鍵,在下面可以找到“p.error”的規(guī)則。div 元素在 ID 表(鍵為 ID)和標(biāo)記表中有相關(guān)的規(guī)則。剩下的工作就是找出哪些根據(jù)鍵提取的規(guī)則是真正匹配的了。
例如,如果 div 的對(duì)應(yīng)規(guī)則如下:
table div {margin:5px}
這條規(guī)則仍然會(huì)從標(biāo)記表中提取出來(lái),因?yàn)殒I是最右邊的選擇器,但這條規(guī)則并不匹配我們的 div 元素,因?yàn)?div 沒有 table 祖先。
Webkit 和 Firefox 都進(jìn)行了這一處理。
樣式對(duì)象具有每個(gè)可視化屬性一一對(duì)應(yīng)的屬性(均為 CSS 屬性但更為通用)。如果某個(gè)屬性未由任何匹配規(guī)則所定義,那么部分屬性就可由父代元素樣式對(duì)象繼承。其他屬性具有默認(rèn)值。
如果定義不止一個(gè),就會(huì)出現(xiàn)問題,需要通過層疊順序來(lái)解決。
1)樣式表層疊順序
某個(gè)樣式屬性的聲明可能會(huì)出現(xiàn)在多個(gè)樣式表中,也可能在同一個(gè)樣式表中出現(xiàn)多次。這意味著應(yīng)用規(guī)則的順序極為重要。這稱為“層疊”順序。根據(jù) CSS2 規(guī)范,層疊的順序?yàn)椋▋?yōu)先級(jí)從低到高):
瀏覽器聲明
用戶普通聲明
作者普通聲明
作者重要聲明
用戶重要聲明
瀏覽器聲明是重要程度最低的,而用戶只有將該聲明標(biāo)記為“重要”才可以替換網(wǎng)頁(yè)作者的聲明。同樣順序的聲明會(huì)根據(jù)特異性進(jìn)行排序,然后再是其指定順序。HTML 可視化屬性會(huì)轉(zhuǎn)換成匹配的 CSS 聲明。它們被視為低優(yōu)先級(jí)的網(wǎng)頁(yè)作者規(guī)則。
2)特異性
選擇器的特異性由 CSS2 規(guī)范定義如下:
如果聲明來(lái)自于“style”屬性,而不是帶有選擇器的規(guī)則,則記為 1,否則記為 0 (= a)
記為選擇器中 ID 屬性的個(gè)數(shù) (= b)
記為選擇器中其他屬性和偽類的個(gè)數(shù) (= c)
記為選擇器中元素名稱和偽元素的個(gè)數(shù) (= d)
將四個(gè)數(shù)字按 a-b-c-d 這樣連接起來(lái)(位于大數(shù)進(jìn)制的數(shù)字系統(tǒng)中),構(gòu)成特異性。
您使用的進(jìn)制取決于上述類別中的最高計(jì)數(shù)。
例如,如果 a=14,您可以使用十六進(jìn)制。如果 a=17,那么您需要使用十七進(jìn)制;當(dāng)然不太可能出現(xiàn)這種情況,除非是存在如下的選擇器:html body div div p …(在選擇器中出現(xiàn)了 17 個(gè)標(biāo)記,這樣的可能性極低)。
一些示例:
- {} /* 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 */
3)規(guī)則排序
找到匹配的規(guī)則之后,應(yīng)根據(jù)級(jí)聯(lián)順序?qū)⑵渑判?。Webkit 對(duì)于較小的列表會(huì)使用冒泡排序,而對(duì)較大的列表則使用歸并排序。對(duì)于以下規(guī)則,Webkit 通過替換“>”運(yùn)算符來(lái)實(shí)現(xiàn)排序:
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;}
Webkit 使用一個(gè)標(biāo)記來(lái)表示是否所有的頂級(jí)樣式表(包括 @imports)均已加載完畢。如果在附加過程中尚未完全加載樣式,則使用占位符,并在文檔中進(jìn)行標(biāo)注,等樣式表加載完畢后再重新計(jì)算。
呈現(xiàn)器在創(chuàng)建完成并添加到渲染樹時(shí),并不包含位置和大小信息。計(jì)算這些值的過程稱為布局或重排。
HTML 采用基于流的布局模型,這意味著大多數(shù)情況下只要一次遍歷就能計(jì)算出幾何信息。處于流中靠后位置元素通常不會(huì)影響靠前位置元素的幾何特征,因此布局可以按從左至右、從上至下的順序遍歷文檔。但是也有例外情況,比如 HTML 表格的計(jì)算就需要不止一次的遍歷 。
坐標(biāo)系是相對(duì)于根框架而建立的,使用的是上坐標(biāo)和左坐標(biāo)。
布局是一個(gè)遞歸的過程。它從根呈現(xiàn)器(對(duì)應(yīng)于 HTML 文檔的 元素)開始,然后遞歸遍歷部分或所有的框架層次結(jié)構(gòu),為每一個(gè)需要計(jì)算的呈現(xiàn)器計(jì)算幾何信息。
根呈現(xiàn)器的位置左邊是 0,0,其尺寸為視口(也就是瀏覽器窗口的可見區(qū)域)。
所有的呈現(xiàn)器都有一個(gè)“l(fā)aybout”或者“reflow”方法,每一個(gè)呈現(xiàn)器都會(huì)調(diào)用其需要進(jìn)行布局的子代的 layout 方法。
為避免對(duì)所有細(xì)小更改都進(jìn)行整體布局,瀏覽器采用了一種“dirty 位”系統(tǒng)。如果某個(gè)呈現(xiàn)器發(fā)生了更改,或者將自身及其子代標(biāo)注為“dirty”,則需要進(jìn)行布局。
有兩種標(biāo)記:“dirty”和“children are dirty”?!癱hildren are dirty”表示盡管呈現(xiàn)器自身沒有變化,但它至少有一個(gè)子代需要布局。
全局布局是指觸發(fā)了整個(gè)渲染樹范圍的布局,觸發(fā)原因可能包括:
影響所有呈現(xiàn)器的全局樣式更改,例如字體大小更改。
屏幕大小調(diào)整。
布局可以采用增量方式,也就是只對(duì) dirty 呈現(xiàn)器進(jìn)行布局(這樣可能存在需要進(jìn)行額外布局的弊端)。
當(dāng)呈現(xiàn)器為 dirty 時(shí),會(huì)異步觸發(fā)增量布局。例如,當(dāng)來(lái)自網(wǎng)絡(luò)的額外內(nèi)容添加到 DOM 樹之后,新的呈現(xiàn)器附加到了渲染樹中。
增量布局是異步執(zhí)行的。Firefox 將增量布局的“reflow 命令”加入隊(duì)列,而調(diào)度程序會(huì)觸發(fā)這些命令的批量執(zhí)行。Webkit 也有用于執(zhí)行增量布局的計(jì)時(shí)器:對(duì)渲染樹進(jìn)行遍歷,并對(duì) dirty 呈現(xiàn)器進(jìn)行布局。
請(qǐng)求樣式信息(例如“offsetHeight”)的腳本可同步觸發(fā)增量布局。
全局布局往往是同步觸發(fā)的。
有時(shí),當(dāng)初始布局完成之后,如果一些屬性(如滾動(dòng)位置)發(fā)生變化,布局就會(huì)作為回調(diào)而觸發(fā)。
如果布局是由“大小調(diào)整”或呈現(xiàn)器的位置(而非大?。└淖兌|發(fā)的,那么可以從緩存中獲取呈現(xiàn)器的大小,而無(wú)需重新計(jì)算。
在某些情況下,只有一個(gè)子樹進(jìn)行了修改,因此無(wú)需從根節(jié)點(diǎn)開始布局。這適用于在本地進(jìn)行更改而不影響周圍元素的情況,例如在文本字段中插入文本(否則每次鍵盤輸入都將觸發(fā)從根節(jié)點(diǎn)開始的布局)。
布局通常具有以下模式:
父呈現(xiàn)器確定自己的寬度。
父呈現(xiàn)器依次處理子呈現(xiàn)器,并且:
放置子呈現(xiàn)器(設(shè)置 x,y 坐標(biāo))。
如果有必要,調(diào)用子呈現(xiàn)器的布局(如果子呈現(xiàn)器是 dirty 的,或者這是全局布局,或出于其他某些原因),這會(huì)計(jì)算子呈現(xiàn)器的高度。
父呈現(xiàn)器根據(jù)子呈現(xiàn)器的累加高度以及邊距和補(bǔ)白的高度來(lái)設(shè)置自身高度,此值也可供父呈現(xiàn)器的父呈現(xiàn)器使用。
將其 dirty 位設(shè)置為 false。
Firefox 使用“state”對(duì)象 (nsHTMLReflowState) 作為布局的參數(shù)(稱為“reflow”),這其中包括了父呈現(xiàn)器的寬度。
Firefox 布局的輸出為“metrics”對(duì)象 (nsHTMLReflowMetrics),其包含計(jì)算得出的呈現(xiàn)器高度。
呈現(xiàn)器寬度是根據(jù)容器塊的寬度、呈現(xiàn)器樣式中的“width”屬性以及邊距和邊框計(jì)算得出的。
例如以下 div 的寬度:
<divstyle="width:30%"/>
將由 Webkit 計(jì)算如下(BenderBox 類,calcWidth 方法):容器的寬度取容器的 availableWidth 和 0 中的較大值。availableWidth 在本例中相當(dāng)于 contentWidth,計(jì)算公式如下:clientWidth()- paddingLeft()- paddingRight()
clientWidth 和 clientHeight 表示一個(gè)對(duì)象的內(nèi)部(除去邊框和滾動(dòng)條)。元素的寬度是“width”樣式屬性。它會(huì)根據(jù)容器寬度的百分比計(jì)算得出一個(gè)絕對(duì)值。
然后加上水平方向的邊框和補(bǔ)白。
現(xiàn)在計(jì)算得出的是“preferred width”。然后需要計(jì)算最小寬度和最大寬度。
如果首選寬度大于最大寬度,那么應(yīng)使用最大寬度。如果首選寬度小于最小寬度(最小的不可破開單位),那么應(yīng)使用最小寬度。
這些值會(huì)緩存起來(lái),以用于需要布局而寬度不變的情況。
如果呈現(xiàn)器在布局過程中需要換行,會(huì)立即停止布局,并告知其父代需要換行。父代會(huì)創(chuàng)建額外的呈現(xiàn)器,并對(duì)其調(diào)用布局。
在繪制階段,系統(tǒng)會(huì)遍歷渲染樹,并調(diào)用呈現(xiàn)器的“paint”方法,將呈現(xiàn)器的內(nèi)容顯示在屏幕上。繪制工作是使用用戶界面基礎(chǔ)組件完成的。
和布局一樣,繪制也分為全局(繪制整個(gè)渲染樹)和增量?jī)煞N。在增量繪制中,部分呈現(xiàn)器發(fā)生了更改,但是不會(huì)影響整個(gè)樹。更改后的呈現(xiàn)器將其在屏幕上對(duì)應(yīng)的矩形區(qū)域設(shè)為無(wú)效,這導(dǎo)致 OS 將其視為一塊“dirty 區(qū)域”,并生成“paint”事件。OS 會(huì)很巧妙地將多個(gè)區(qū)域合并成一個(gè)。在 Chrome 瀏覽器中,情況要更復(fù)雜一些,因?yàn)?Chrome 瀏覽器的呈現(xiàn)器不在主進(jìn)程上。Chrome 瀏覽器會(huì)在某種程度上模擬 OS 的行為。展示層會(huì)偵聽這些事件,并將消息委托給呈現(xiàn)根節(jié)點(diǎn)。然后遍歷渲染樹,直到找到相關(guān)的呈現(xiàn)器,該呈現(xiàn)器會(huì)重新繪制自己(通常也包括其子代)。
CSS2 規(guī)范定義了繪制流程的順序。繪制的順序其實(shí)就是元素進(jìn)入堆棧樣式上下文的順序。這些堆棧會(huì)從后往前繪制,因此這樣的順序會(huì)影響繪制。塊呈現(xiàn)器的堆棧順序如下:
背景顏色
背景圖片
邊框
子代
輪廓
Firefox 遍歷整個(gè)渲染樹,為繪制的矩形建立一個(gè)顯示列表。列表中按照正確的繪制順序(先是呈現(xiàn)器的背景,然后是邊框等等)包含了與矩形相關(guān)的呈現(xiàn)器。這樣等到重新繪制的時(shí)候,只需遍歷一次渲染樹,而不用多次遍歷(繪制所有背景,然后繪制所有圖片,再繪制所有邊框等等)。
Firefox 對(duì)此過程進(jìn)行了優(yōu)化,也就是不添加隱藏的元素,例如被不透明元素完全遮擋住的元素。
在重新繪制之前,Webkit 會(huì)將原來(lái)的矩形另存為一張位圖,然后只繪制新舊矩形之間的差異部分。
在發(fā)生變化時(shí),瀏覽器會(huì)盡可能做出最小的響應(yīng)。因此,元素的顏色改變后,只會(huì)對(duì)該元素進(jìn)行重繪。元素的位置改變后,只會(huì)對(duì)該元素及其子元素(可能還有同級(jí)元素)進(jìn)行布局和重繪。添加 DOM 節(jié)點(diǎn)后,會(huì)對(duì)該節(jié)點(diǎn)進(jìn)行布局和重繪。一些重大變化(例如增大“html”元素的字體)會(huì)導(dǎo)致緩存無(wú)效,使得整個(gè)渲染樹都會(huì)進(jìn)行重新布局和繪制。
渲染引擎采用了單線程。幾乎所有操作(除了網(wǎng)絡(luò)操作)都是在單線程中進(jìn)行的。在 Firefox 和 Safari 中,該線程就是瀏覽器的主線程。而在 Chrome 瀏覽器中,該線程是標(biāo)簽進(jìn)程的主線程。
網(wǎng)絡(luò)操作可由多個(gè)并行線程執(zhí)行。并行連接數(shù)是有限的(通常為 2 至 6 個(gè),以 Firefox 3 為例是 6 個(gè))。
事件循環(huán)
瀏覽器的主線程是事件循環(huán)。它是一個(gè)無(wú)限循環(huán),永遠(yuǎn)處于接受處理狀態(tài),并等待事件(如布局和繪制事件)發(fā)生,并進(jìn)行處理。這是 Firefox 中關(guān)于主事件循環(huán)的代碼:
while(!mExiting) NS_ProcessNextEvent(thread);
根據(jù) CSS2 規(guī)范,“畫布”這一術(shù)語(yǔ)是指“用來(lái)呈現(xiàn)格式化結(jié)構(gòu)的空間”,也就是供瀏覽器繪制內(nèi)容的區(qū)域。畫布的空間尺寸大小是無(wú)限的,但是瀏覽器會(huì)根據(jù)視口的尺寸選擇一個(gè)初始寬度。
根據(jù) www.w3.org/TR/CSS2/zindex.html,畫布如果包含在其他畫布內(nèi),就是透明的;否則會(huì)由瀏覽器指定一種顏色。
CSS 框模型描述的是針對(duì)文檔樹中的元素而生成,并根據(jù)可視化格式模型進(jìn)行布局的矩形框。
每個(gè)框都有一個(gè)內(nèi)容區(qū)域(例如文本、圖片等),還有可選的周圍補(bǔ)白、邊框和邊距區(qū)域。
每一個(gè)節(jié)點(diǎn)都會(huì)生成 0..n 個(gè)這樣的框。 所有元素都有一個(gè)“display”屬性,決定了它們所對(duì)應(yīng)生成的框類型。示例:
block - generates a block box.inline - generates one or more inline boxes.none - no box is generated.
默認(rèn)值是 inline,但是瀏覽器樣式表設(shè)置了其他默認(rèn)值。例如,“div”元素的 display 屬性默認(rèn)值是 block。
您可以在這里找到默認(rèn)樣式表示例:www.w3.org/TR/CSS2/sample.html
有三種定位方案:
普通:根據(jù)對(duì)象在文檔中的位置進(jìn)行定位,也就是說對(duì)象在渲染樹中的位置和它在 DOM 樹中的位置相似,并根據(jù)其框類型和尺寸進(jìn)行布局。
浮動(dòng):對(duì)象先按照普通流進(jìn)行布局,然后盡可能地向左或向右移動(dòng)。
絕對(duì):對(duì)象在渲染樹中的位置和它在 DOM 樹中的位置不同。
定位方案是由“position”屬性和“l(fā)oat”屬性設(shè)置的。
如果值是 static 和 relative,就是普通流
如果值是 absolute 和 fixed,就是絕對(duì)定位
static 定位無(wú)需定義位置,而是使用默認(rèn)定位。對(duì)于其他方案,網(wǎng)頁(yè)作者需要指定位置:top、bottom、left、right。
框的布局方式是由以下因素決定的:
框尺寸
定位方案
外部信息,例如圖片大小和屏幕大小
框類型
block 框:形成一個(gè) block,在瀏覽器窗口中擁有其自己的矩形區(qū)域。
inline 框放置在行中或“行框”中。這些行至少和最高的框一樣高,還可以更高,當(dāng)框根據(jù)“底線”對(duì)齊時(shí),這意味著元素的底部需要根據(jù)其他框中非底部的位置對(duì)齊。如果容器的寬度不夠,inline 元素就會(huì)分為多行放置。在段落中經(jīng)常發(fā)生這種情況。
相對(duì)定位:先按照普通方式定位,然后根據(jù)所需偏移量進(jìn)行移動(dòng)。
浮動(dòng)框會(huì)移動(dòng)到行的左邊或右邊。有趣的特征在于,其他框會(huì)浮動(dòng)在它的周圍。下面這段 HTML 代碼:
<p> <img style="float:right" src="images/image.gif" width="100" height="100"> Lorem ipsum dolor sit amet, consectetuer...</p>
顯示效果如下:
這種布局是準(zhǔn)確定義的,與普通流無(wú)關(guān)。元素不參與普通流。尺寸是相對(duì)于容器而言的。在固定定位中,容器就是可視區(qū)域。
請(qǐng)注意,即使在文檔滾動(dòng)時(shí),固定框也不會(huì)移動(dòng)。
這是由 z-index CSS 屬性指定的。它代表了框的第三個(gè)維度,也就是沿“z 軸”方向的位置。
這些框分散到多個(gè)堆棧(稱為堆棧上下文)中。在每一個(gè)堆棧中,會(huì)首先繪制后面的元素,然后在頂部繪制前面的元素,以便更靠近用戶。如果出現(xiàn)重疊,新繪制的元素就會(huì)覆蓋之前的元素。
堆棧是按照 z-index 屬性進(jìn)行排序的。具有“z-index”屬性的框形成了本地堆棧。視口具有外部堆棧。示例:
<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>
結(jié)果如下:
雖然紅色 div 在標(biāo)記中的位置比綠色 div 靠前(按理應(yīng)該在常規(guī)流程中優(yōu)先繪制),但是 z-index 屬性的優(yōu)先級(jí)更高,因此它移動(dòng)到了根框所保持的堆棧中更靠前的位置。
聯(lián)系客服