原文地址css
這是一篇全面介紹 WebKit 和 Gecko 內部操做的入門文章,是以色列開發人員塔利·加希爾大量研究的成果。在過去的幾年中,她查閱了全部公開發布的關於瀏覽器內部機制的數據(請參見資源),並花了不少時間來研讀網絡瀏覽器的源代碼。她寫道:html
在 IE 佔據 90% 市場份額的年代,咱們除了把瀏覽器當成一個「黑箱」,什麼也作不了。可是如今,開放源代碼的瀏覽器擁有了 過半的市場份額,所以,是時候來揭開神祕的面紗,一探網絡瀏覽器的內幕了。呃,裏面只有數以百萬行計的 C++ 代碼...塔利在 她的網站上公佈了本身的研究成果,可是咱們以爲它值得讓更多的人來了解,因此咱們在此從新整理並公佈。
做爲一名網絡開發人員,學習瀏覽器的內部工做原理將有助於您做出更明智的決策,並理解那些最佳開發實踐的箇中原因。儘管這是一篇至關長的文檔,可是咱們建議您花些時間來仔細閱讀;讀完以後,您確定會以爲所費不虛。保羅·愛麗詩 (Paul Irish),Chrome 瀏覽器開發人員事務部前端
網絡瀏覽器極可能是使用最廣的軟件。在這篇入門文章中,我將會介紹它們的幕後工做原理。咱們會了解到,從您在地址欄輸入 google.com
直到您在瀏覽器屏幕上看到 Google 首頁的整個過程當中都發生了些什麼。html5
目前使用的主流瀏覽器有五個:Internet Explorer、Firefox、Safari、Chrome 瀏覽器和 Opera。本文中以開放源代碼瀏覽器爲例,即 Firefox、Chrome 瀏覽器和 Safari(部分開源)。根據 StatCounter 瀏覽器統計數據,目前(2011 年 8 月)Firefox、Safari 和 Chrome 瀏覽器的總市場佔有率將近 60%。因而可知,現在開放源代碼瀏覽器在瀏覽器市場中佔據了很是堅實的部分。node
瀏覽器的主要功能就是向服務器發出請求,在瀏覽器窗口中展現您選擇的網絡資源。這裏所說的資源通常是指 HTML 文檔,也能夠是 PDF、圖片或其餘的類型。資源的位置由用戶使用 URI(統一資源標示符)指定。web
瀏覽器解釋並顯示 HTML 文件的方式是在 HTML 和 CSS 規範中指定的。這些規範由網絡標準化組織 W3C(萬維網聯盟)進行維護。
多年以來,各瀏覽器都沒有徹底聽從這些規範,同時還在開發本身獨有的擴展程序,這給網絡開發人員帶來了嚴重的兼容性問題。現在,大多數的瀏覽器都是或多或少地聽從規範。正則表達式
瀏覽器的用戶界面有不少彼此相同的元素,其中包括:算法
奇怪的是,瀏覽器的用戶界面並無任何正式的規範,這是多年來的最佳實踐天然發展以及彼此之間相互模仿的結果。HTML5 也沒有定義瀏覽器必須具備的用戶界面元素,但列出了一些通用的元素,例如地址欄、狀態欄和工具欄等。固然,各瀏覽器也能夠有本身獨特的功能,好比 Firefox 的下載管理器。shell
瀏覽器的主要組件爲 (1.1):數據庫
值得注意的是,和大多數瀏覽器不一樣,Chrome 瀏覽器的每一個標籤(tab)都分別對應一個呈現引擎實例。每一個標籤(tab)都是一個獨立的進程。
呈現引擎(渲染引擎 rendering engine)的做用嘛...固然就是「呈現」了,也就是在瀏覽器的屏幕上顯示請求的內容。
默認狀況下,呈現引擎可顯示 HTML 和 XML 文檔與圖片。經過插件(或瀏覽器擴展程序),還能夠顯示其餘類型的內容;例如,使用 PDF 查看器插件就能顯示 PDF 文檔。可是在本章中,咱們將集中介紹其主要用途:顯示使用 CSS 格式化的 HTML 內容和圖片。
本文所討論的瀏覽器(Firefox、Chrome 瀏覽器和 Safari)是基於兩種呈現引擎構建的。Firefox 使用的是 Gecko,這是 Mozilla 公司「自制」的呈現引擎。而 Safari 和 Chrome 瀏覽器使用的都是 WebKit。
WebKit 是一種開放源代碼呈現引擎,起初用於 Linux 平臺,隨後由 Apple 公司進行修改,從而支持蘋果機和 Windows。有關詳情,請參閱 webkit.org。
呈現引擎一開始會從網絡層獲取請求文檔的內容,內容的大小通常限制在 8000 個塊之內。
而後進行以下所示的基本流程:
呈現引擎將開始解析 HTML 文檔,並將各標記逐個轉化成「內容樹」上的 DOM 節點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數據。HTML 中這些帶有視覺指令的樣式信息將用於建立另外一個樹結構:呈現樹(渲染樹)。
呈現樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序。
呈現樹構建完畢以後,進入「佈局」處理階段,也就是爲每一個節點分配一個應出如今屏幕上的確切座標。下一個階段是繪製 - 呈現引擎會遍歷呈現樹,由用戶界面後端層將每一個節點繪製出來。
須要着重指出的是,這是一個漸進的過程。爲達到更好的用戶體驗,呈現引擎會力求儘快將內容顯示在屏幕上。它沒必要等到整個 HTML 文檔解析完畢,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其他內容的同時,呈現引擎會將部份內容解析並顯示出來。
從圖 3 和圖 4 能夠看出,雖然 WebKit 和 Gecko 使用的術語略有不一樣,但總體流程是基本相同的。
Gecko 將視覺格式化元素組成的樹稱爲「框架樹」。每一個元素都是一個框架。WebKit 使用的術語是「呈現樹」,它由「呈現對象」組成。對於元素的放置,WebKit 使用的術語是「佈局」,而 Gecko 稱之爲「重排」。對於鏈接 DOM 節點和可視化信息從而建立呈現樹的過程,WebKit 使用的術語是「附加」。有一個細微的非語義差異,就是 Gecko 在 HTML 與 DOM 樹之間還有一個稱爲「內容槽」的層,用於生成 DOM 元素。咱們會逐一論述流程中的每一部分:
筆記:從圖中能夠看出,渲染樹是由DOM樹和CSS 規則樹構建的。
解析是呈現引擎中很是重要的一個環節,所以咱們要更深刻地講解。首先,來介紹一下解析。
解析文檔是指將文檔轉化成爲有意義的結構,也就是可以讓代碼理解和使用的結構。解析獲得的結果一般是表明了文檔結構的節點樹,它稱做解析樹或者語法樹。
示例 - 解析 2 + 3 - 1 這個表達式,會返回下面的樹:
解析是以文檔所遵循的語法規則(編寫文檔所用的語言或格式)爲基礎的。全部能夠解析的格式都必須對應肯定的語法(由詞彙和語法規則構成)。這稱爲與上下文無關的語法。人類語言並不屬於這樣的語言,所以沒法用常規的解析技術進行解析。
解析(parsing)的過程能夠分紅兩個子過程:詞法分析(lexical analysis )和語法分析(syntax analysis)。
詞法分析是將輸入內容分割成大量標記的過程。標記是語言中的詞彙,即構成內容的單位。在人類語言中,它至關於語言字典中的單詞。
語法分析是應用語言的語法規則的過程。
解析器(parser)一般將解析工做分給如下兩個組件來處理:詞法分析器(lexer)(有時也稱爲標記生成器),負責將輸入內容分解成一個個有效標記;而解析器(parser)負責根據語言的語法規則分析文檔的結構,從而構建解析樹。詞法分析器知道如何將無關的字符(好比空格和換行符)分離出來。
解析是一個迭代的過程。一般,解析器會向詞法分析器請求一個新標記,並嘗試將其與某條語法規則進行匹配。若是發現了匹配規則,解析器會將一個對應於該標記的節點添加到解析樹中,而後繼續請求下一個標記。
若是沒有規則能夠匹配,解析器就會將標記存儲到內部,並繼續請求標記,直至找到可與全部內部存儲的標記匹配的規則。若是找不到任何匹配規則,解析器就會引起一個異常。這意味着文檔無效,包含語法錯誤。
筆記:個人理解,詞法分析器根據文檔的類型從頭至尾閱讀文檔,將文檔中的內容理解成一個個的「單詞」,解析器根據文檔所使用的語言的語法規則將這一個個的「單詞」歸爲不一樣類型的節點(好比元素節點、文本節點、屬性節點等)並肯定這些節點之間的關係,而後以此爲基礎構建解析樹。
不少時候,解析樹還不是最終產品。解析一般是在翻譯過程當中使用的,而翻譯是指將輸入文檔轉換成另外一種格式。編譯就是這樣一個例子。編譯器可將源代碼編譯成機器代碼,具體過程是首先將源代碼解析成解析樹,而後將解析樹翻譯成機器代碼文檔。
筆記:將解析樹翻譯成機器碼
在圖 5 中,咱們經過一個數學表達式創建瞭解析樹。如今,讓咱們試着定義一個簡單的數學語言,用來演示解析的過程。
詞彙:咱們用的語言可包含整數、加號和減號。
語法:
讓咱們分析一下 2 + 3 - 1。
匹配語法規則的第一個子串是 2,而根據第 5 條語法規則,這是一個項。匹配語法規則的第二個子串是 2 + 3,而根據第 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 格式表達的語法。有關正式定義,請參閱關於與上下文無關的語法的維基百科文章。
有兩種基本類型的解析器:自上而下解析器和自下而上解析器。直觀地來講,自上而下的解析器從語法的高層結構出發(由表達式到項或運算符),嘗試從中找到匹配的結構。而自下而上的解析器(從項或運算符到表達式)從低層規則出發,將輸入內容逐步轉化爲語法規則,直至知足高層規則。
讓咱們來看看這兩種解析器會如何解析咱們的示例:
自上而下的解析器會從高層的規則開始:首先將 2 + 3 標識爲一個表達式,而後將 2 + 3 - 1 標識爲一個表達式(標識表達式的過程涉及到匹配其餘規則,可是起點是最高級別的規則)。
自下而上的解析器將掃描輸入內容,找到匹配的規則後,將匹配的輸入內容替換成規則。如此繼續替換,直到輸入內容的結尾。部分匹配的表達式保存在解析器的堆棧中。
堆棧 | 輸入 |
---|---|
2 + 3 - 1 | |
項(2) | + 3 - 1 |
項運算(2+) | 3 - 1 |
表達式(2+3) | - 1 |
表達式運算符(2+3-) | 1 |
表達式(2+3-1) |
筆記:我以爲這個表應該這麼畫
堆棧 | 輸入 |
表達式 | |
表達式運算符 | 1 |
表達式 | -1 |
項運算 | 3-1 |
項 | +3-1 |
2+3-1 |
首先輸入2+3-1,因爲自下而上的解析器從低層規則出發,因此解析器先解析2,將他與語法規則匹配,換成語法規則中的項,而後解析+,替換成運算符,以此類推。
有一些工具能夠幫助您生成解析器,它們稱爲解析器生成器。您只要向其提供您所用語言的語法(詞彙和語法規則),它就會生成相應的解析器。建立解析器須要對解析有深入理解,而人工建立並優化解析器並非一件容易的事情,因此解析器生成器是很是實用的。
WebKit 使用了兩種很是有名的解析器生成器:用於建立詞法分析器的 Flex 以及用於建立解析器的 Bison(您也可能遇到 Lex 和 Yacc 這樣的別名)。Flex 的輸入是包含標記的正則表達式定義的文件。Bison 的輸入是採用 BNF 格式的語言語法規則。
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 如此流行的緣由:它能包容您的錯誤,簡化網絡開發。另外一方面,這使得它很難編寫正式的語法。歸納地說,HTML 沒法很容易地經過常規解析器解析(由於它的語法不是與上下文無關的語法),也沒法經過 XML 解析器來解析。
HTML 的定義採用了 DTD 格式。此格式可用於定義 SGML 族的語言。它包括全部容許使用的元素及其屬性和層次結構的定義。如上文所述,HTML DTD 沒法構成與上下文無關的語法。
DTD 存在一些變體。嚴格模式徹底遵照 HTML 規範,而其餘模式可支持之前的瀏覽器所使用的標記。這樣作的目的是確保向下兼容一些早期版本的內容。最新的嚴格模式 DTD 能夠在這裏找到:www.w3.org/TR/html4/strict.dtd
解析器的輸出的「解析樹」是由 DOM 元素和屬性節點構成的樹結構。DOM 是文檔對象模型 (Document Object Model) 的縮寫。它是 HTML 文檔的對象表示,同時也是外部內容(例如 JavaScript)與 HTML 元素之間的接口。
解析樹的根節點是「Document」對象。
DOM 與標記之間幾乎是一一對應的關係。好比下面這段標記:
<html> <body> <p> Hello World </p> <div> <img src="example.png"/></div> </body> </html>
和 HTML 同樣,DOM 也是由 W3C 組織指定的。請參見 www.w3.org/DOM/DOMTR。這是關於文檔操做的通用規範。其中一個特定模塊描述針對 HTML 的元素。HTML 的定義能夠在這裏找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
我所說的樹包含 DOM 節點,指的是樹是由實現了某個 DOM 接口的元素構成的。瀏覽器在具體的實現中會有一些供內部使用的其餘屬性。
咱們在以前章節已經說過,HTML 沒法用常規的自上而下或自下而上的解析器進行解析。
緣由在於:
document.write
,就會添加額外的標記,這樣解析過程實際上就更改了輸入內容。因爲不能使用常規的解析技術,瀏覽器就建立了自定義的解析器來解析 HTML。
HTML5 規範詳細地描述瞭解析算法。此算法由兩個階段組成:標記化和樹構建。
標記化是詞法分析過程,將輸入內容解析成多個標記。HTML 標記包括起始標記、結束標記、屬性名稱和屬性值。
標記生成器識別標記,傳遞給樹構造器,而後接受下一個字符以識別下一個標記;如此反覆直到輸入的結束。
該算法的輸出結果是 HTML 標記。該算法使用狀態機來表示。每個狀態接收來自輸入信息流的一個或多個字符,並根據這些字符更新下一個狀態。當前的標記化狀態和樹結構狀態會影響進入下一狀態的決定。這意味着,即便接收的字符相同,對於下一個正確的狀態也會產生不一樣的結果,具體取決於當前的狀態。該算法至關複雜,沒法在此詳述,因此咱們經過一個簡單的示例來幫助你們理解其原理。
基本示例 - 將下面的 HTML 代碼標記化:
<html> <body> Hello world </body> </html>
初始狀態是數據狀態。遇到字符 <
時,狀態更改成「標記打開狀態」。接收一個 a-z
字符會建立「起始標記」,狀態更改成「標記名稱狀態」。這個狀態會一直保持到接收 >
字符。在此期間接收的每一個字符都會附加到新的標記名稱上。在本例中,咱們建立的標記是 html
標記。
遇到 >
標記時,會發送當前的標記,狀態改回「數據狀態」。<body>
標記也會進行一樣的處理。目前 html
和 body
標記均已發出。如今咱們回到「數據狀態」。接收到 Hello world
中的 H
字符時,將建立併發送字符標記,直到接收 </body>
中的 <
。咱們將爲 Hello world
中的每一個字符都發送一個字符標記。
如今咱們回到「標記打開狀態」。接收下一個輸入字符 /
時,會建立 end tag token
並改成「標記名稱狀態」。咱們會再次保持這個狀態,直到接收 >
。而後將發送新的標記,並回到「數據狀態」。</html>
輸入也會進行一樣的處理。
在建立解析器的同時,也會建立 Document 對象。在樹構建階段,以 Document 爲根節點的 DOM 樹也會不斷進行修改,向其中添加各類元素。標記生成器發送的每一個節點都會由樹構建器進行處理。規範中定義了每一個標記所對應的 DOM 元素,這些元素會在接收到相應的標記時建立。這些元素不只會添加到 DOM 樹中,還會添加到開放元素的堆棧中。此堆棧用於糾正嵌套錯誤和處理未關閉的標記。其算法也能夠用狀態機來描述。這些狀態稱爲「插入模式」。
讓咱們來看看示例輸入的樹構建過程:
<html> <body> Hello world </body> </html>
樹構建階段的輸入是一個來自標記化階段的標記序列。第一個模式是「initial mode」。接收 HTML 標記後轉爲「before html」模式,並在這個模式下從新處理此標記。這樣會建立一個 HTMLHtmlElement 元素,並將其附加到 Document 根對象上。
而後狀態將改成「before head」。此時咱們接收「body」標記。即便咱們的示例中沒有「head」標記,系統也會隱式建立一個 HTMLHeadElement,並將其添加到樹中。
如今咱們進入了「in head」模式,而後轉入「after head」模式。系統對 body 標記進行從新處理,建立並插入 HTMLBodyElement,同時模式轉變爲「in body」。
如今,接收由「Hello world」字符串生成的一系列字符標記。接收第一個字符時會建立並插入「Text」節點,而其餘字符也將附加到該節點。
接收 body 結束標記會觸發「after body」模式。如今咱們將接收 HTML 結束標記,而後進入「after after body」模式。接收到文件結束標記後,解析過程就此結束。
在此階段,瀏覽器會將文檔標註爲交互狀態,並開始解析那些處於「deferred」模式的腳本,也就是那些應在文檔解析完成後才執行的腳本。而後,文檔狀態將設置爲「完成」,一個「加載」事件將隨之觸發。
您在瀏覽 HTML 網頁時歷來不會看到「語法無效」的錯誤。這是由於瀏覽器會糾正任何無效內容,而後繼續工做。
如下面的 HTML 代碼爲例:
<html> <mytag> </mytag> <div> <p> </div> Really lousy HTML </p> </html>
在這裏,我已經違反了不少語法規則(「mytag」不是標準的標記,「p」和「div」元素之間的嵌套有誤等等),可是瀏覽器仍然會正確地顯示這些內容,而且毫無怨言。由於有大量的解析器代碼會糾正 HTML 網頁做者的錯誤。
不一樣瀏覽器的錯誤處理機制至關一致,但使人稱奇的是,這種機制並非 HTML 當前規範的一部分。和書籤管理以及前進/後退按鈕同樣,它也是瀏覽器在多年發展中的產物。不少網站都廣泛存在着一些已知的無效 HTML 結構,每一種瀏覽器都會嘗試經過和其餘瀏覽器同樣的方式來修復這些無效結構。
HTML5 規範定義了一部分這樣的要求。WebKit 在 HTML 解析器類的開頭註釋中對此作了很好的歸納。
解析器對標記化輸入內容進行解析,以構建文檔樹。若是文檔的格式正確,就直接進行解析。
遺憾的是,咱們不得不處理不少格式錯誤的 HTML 文檔,因此解析器必須具有必定的容錯性。
咱們至少要可以處理如下錯誤狀況:
- 明顯不能在某些外部標記中添加的元素。在此狀況下,咱們應該關閉全部標記,直到出現禁止添加的元素,而後再加入該元素。
- 咱們不能直接添加的元素。這極可能是網頁做者忘記添加了其中的一些標記(或者其中的標記是可選的)。這些標籤可能包括:HTML HEAD BODY TBODY TR TD LI(還有遺漏的嗎?)。
- 向 inline 元素內添加 block 元素。關閉全部 inline 元素,直到出現下一個較高級的 block 元素。
- 若是這樣仍然無效,可關閉全部元素,直到能夠添加元素爲止,或者忽略該標記。
讓咱們看一些 WebKit 容錯的示例:
有些網站使用了 </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>
<table> <tr><td>outer table</td></tr> </table> <table> <tr><td>inner table</td></tr> </table>
if (m_inStrayTableContent && localName == tableTag) popBlock(tableTag);
若是用戶在一個表單元素中又放入了另外一個表單,那麼第二個表單將被忽略。
代碼以下:
if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document); }
代碼的註釋已經說得很清楚了。
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 標記,由於一些愚蠢的網頁會在實際文檔結束以前就關閉。咱們經過調用 end() 來執行關閉操做。
if (t->tagName == htmlTag || t->tagName == bodyTag ) return;
還記得簡介中解析的概念嗎?和 HTML 不一樣,CSS 是上下文無關的語法,可使用簡介中描述的各類解析器進行解析。事實上,CSS 規範定義了 CSS 的詞法和語法。
讓咱們來看一些示例:
詞法語法(詞彙)是針對各個標記用正則表達式定義的:
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」是標識符 (identifier) 的縮寫,好比類名。「name」是元素的 ID(經過「#」來引用)。
語法是採用 BNF 格式描述的。
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
div.error , a.error { color:red; font-weight:bold; }
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
WebKit 使用 Flex 和 Bison 解析器生成器,經過 CSS 語法文件自動建立解析器。正如咱們以前在解析器簡介中所說,Bison 會建立自下而上的移位歸約解析器。Firefox 使用的是人工編寫的自上而下的解析器。這兩種解析器都會將 CSS 文件解析成 StyleSheet 對象,且每一個對象都包含 CSS 規則。CSS 規則對象則包含選擇器和聲明對象,以及其餘與 CSS 語法對應的對象。
網絡的模型是同步的。網頁做者但願解析器遇到 <script> 標記時當即解析並執行腳本。文檔的解析將中止,直到腳本執行完畢。若是腳本是外部的,那麼解析過程會中止,直到從網絡同步抓取資源完成後再繼續。此模型已經使用了多年,也在 HTML4 和 HTML5 規範中進行了指定。做者也能夠將腳本標註爲「defer」,這樣它就不會中止文檔解析,而是等到解析結束才執行。HTML5 增長了一個選項,可將腳本標記爲異步,以便由其餘線程解析和執行。
筆記:默認狀況下,腳本的執行(好比放在文檔中間的腳本)會阻止文檔的解析。固然開發者也能夠指定腳本爲異步執行。
WebKit 和 Firefox 都進行了這項優化。在執行腳本時,其餘線程會解析文檔的其他部分,找出並加載須要經過網絡加載的其餘資源。經過這種方式,資源能夠在並行鏈接上加載,從而提升整體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工做交由主解析器處理;預解析器只會解析外部資源(例如外部腳本、樣式表和圖片)的引用。
筆記:預解析器解析外部資源,主解析器解析文檔主體。
另外一方面,樣式表有着不一樣的模型。理論上來講,應用樣式表不會更改 DOM 樹,所以彷佛沒有必要等待樣式表並中止文檔解析。但這涉及到一個問題,就是腳本在文檔解析階段會請求樣式信息。若是當時尚未加載和解析樣式,腳本就會得到錯誤的回覆,這樣顯然會產生不少問題。這看上去是一個非典型案例,但事實上很是廣泛。Firefox 在樣式表加載和解析的過程當中,會禁止全部腳本。而對於 WebKit 而言,僅當腳本嘗試訪問的樣式屬性可能受還沒有加載的樣式表影響時,它纔會禁止該腳本。
在 DOM 樹構建的同時,瀏覽器還會構建另外一個樹結構:呈現樹。這是由可視化元素按照其顯示順序而組成的樹,也是文檔的可視化表示。它的做用是讓您按照正確的順序繪製內容。
Firefox 將呈現樹中的元素稱爲「框架」。WebKit 使用的術語是呈現器或呈現對象。
呈現器知道如何佈局並將自身及其子元素繪製出來。
WebKits RenderObject 類是全部呈現器的基類,其定義以下:
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 }
每個呈現器都表明了一個矩形的區域,一般對應於相關節點的 CSS 框,這一點在 CSS2 規範中有所描述。它包含諸如寬度、高度和位置等幾何信息。
框的類型會受到與節點相關的「display」樣式屬性的影響(請參閱樣式計算章節)。下面這段 WebKit 代碼描述了根據 display 屬性的不一樣,針對同一個 DOM 節點應建立什麼類型的呈現器。
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; }
createRenderer
方法。呈現器所指向的樣式對象中包含了一些和幾何無關的信息。
有一些 DOM 元素對應多個可視化對象。它們每每是具備複雜結構的元素,沒法用單一的矩形來描述。例如,「select」元素有 3 個呈現器:一個用於顯示區域,一個用於下拉列表框,還有一個用於按鈕。若是因爲寬度不夠,文本沒法在一行中顯示而分爲多行,那麼新的行也會做爲新的呈現器而添加。
另外一個關於多呈現器的例子是格式無效的 HTML。根據 CSS 規範,inline 元素只能包含 block 元素或 inline 元素中的一種。若是出現了混合內容,則應建立匿名的 block 呈現器,以包裹 inline 元素。
筆記:就是說若是在一個行盒中有行內元素,還有塊級元素(不是行內塊),那麼瀏覽器會將行這個行盒中的行內元素和塊級元素分開來,行內元素用匿名塊包裹,塊級元素就按塊級元素對待,原來的行盒不復存在或者說被拆分了。
有一些呈現對象對應於 DOM 節點,但在樹中所在的位置與 DOM 節點不一樣。浮動定位和絕對定位的元素就是這樣,它們處於正常的流程以外,放置在樹中的其餘地方,並映射到真正的框架,而放在原位的是佔位框架。
在 Firefox 中,系統會針對 DOM 更新註冊展現層,做爲偵聽器。展現層將框架建立工做委託給 FrameConstructor
,由該構造器解析樣式(請參閱樣式計算)並建立框架。
在 WebKit 中,解析樣式和建立呈現器的過程稱爲「附加」。每一個 DOM 節點都有一個「attach」方法。附加是同步進行的,將節點插入 DOM 樹須要調用新的節點「attach」方法。
處理 html 和 body 標記就會構建呈現樹根節點。這個根節點呈現對象對應於 CSS 規範中所說的容器 block,這是最上層的 block,包含了其餘全部 block。它的尺寸就是視口,即瀏覽器窗口顯示區域的尺寸。Firefox 稱之爲 ViewPortFrame
,而 WebKit 稱之爲 RenderView
。這就是文檔所指向的呈現對象。呈現樹的其他部分以 DOM 樹節點插入的形式來構建。
請參閱關於處理模型的 CSS2 規範。
筆記:呈現樹(渲染樹)的構建與DOM樹(文檔的解析,主解析器)和CSS規則對象(樣式表的解析,預解析器)是同步的。
構建呈現樹時,須要計算每個呈現對象的可視化屬性。這是經過計算每一個元素的樣式屬性來完成的。
樣式包括來自各類來源的樣式表、inline 樣式元素和 HTML 中的可視化屬性(例如「bgcolor」屬性)。其中後者將通過轉化以匹配 CSS 樣式屬性。
樣式表的來源包括瀏覽器的默認樣式表、由網頁做者提供的樣式表以及由瀏覽器用戶提供的用戶樣式表(瀏覽器容許您定義本身喜歡的樣式。以 Firefox 爲例,用戶能夠將本身喜歡的樣式表放在「Firefox Profile」文件夾下)。
樣式計算存在如下難點:
若是不進行優化,爲每個元素查找匹配的規則會形成性能問題。要爲每個元素遍歷整個規則列表來尋找匹配規則,這是一項浩大的工程。選擇器會具備很複雜的結構,這就會致使某個匹配過程一開始看起來極可能是正確的,但最終發現實際上是徒勞的,必須嘗試其餘匹配路徑。
例以下面這個組合選擇器:
div div div div{ ... }
這意味着規則適用於做爲 3 個 div 元素的子代的 <div>
。若是您要檢查規則是否適用於某個指定的 <div>
元素,應選擇樹上的一條向上路徑進行檢查。您可能須要向上遍歷節點樹,結果發現只有兩個 div,並且規則並不適用。而後,您必須嘗試樹中的其餘路徑。WebKit 節點會引用樣式對象 (RenderStyle)。這些對象在某些狀況下能夠由不一樣節點共享。這些節點是同級關係,而且:
爲了簡化樣式計算,Firefox 還採用了另外兩種樹:規則樹(the rule tree)和樣式上下文樹(style context tree)。WebKit 也有樣式對象,但它們不是保存在相似樣式上下文樹這樣的樹結構中,只是由 DOM 節點指向此類對象的相關樣式。
樣式上下文包含端值。要計算出這些值,應按照正確順序應用全部的匹配規則,並將其從邏輯值轉化爲具體的值。例如,若是邏輯值是屏幕大小的百分比,則須要換算成絕對的單位。規則樹的點子真的很巧妙,它使得節點之間能夠共享這些值,以免重複計算,還能夠節約空間。
全部匹配的規則都存儲在樹中。路徑中的底層節點擁有較高的優先級。規則樹包含了全部已知規則匹配的路徑。規則的存儲是延遲進行的。規則樹不會在開始的時候就爲全部的節點進行計算,而是隻有當某個節點樣式須要進行計算時,纔會向規則樹添加計算的路徑。
這個想法至關於將規則樹路徑視爲詞典中的單詞。若是咱們已經計算出以下的規則樹:
筆記:規則樹是經過解析樣式表獲得的。
讓咱們看看規則樹如何幫助咱們減小工做。
樣式上下文可分割成多個結構。這些結構體包含了特定類別(如 border 或 color)的樣式信息。結構中的屬性都是繼承的或非繼承的。繼承屬性若是未由元素定義,則繼承自其父代。非繼承屬性(也稱爲「重置」屬性)若是未進行定義,則使用默認值。
規則樹經過緩存整個結構(包含計算出的端值)爲咱們提供幫助。這一想法假定底層節點沒有提供結構的定義,則可以使用上層節點中的緩存結構。
在計算某個特定元素的樣式上下文時,咱們首先計算規則樹中的對應路徑,或者使用現有的路徑。而後咱們沿此路徑應用規則,在新的樣式上下文中填充結構。咱們從路徑中擁有最高優先級的底層節點(一般也是最特殊的選擇器)開始,並向上遍歷規則樹,直到結構填充完畢。若是該規則節點對於此結構沒有任何規範,那麼咱們能夠實現更好的優化:尋找路徑更上層的節點,找到後指定完整的規範並指向相關節點便可。這是最好的優化方法,由於整個結構都能共享。這能夠減小端值的計算量並節約內存。
若是咱們找到了部分定義,就會向上遍歷規則樹,直到結構填充完畢。
若是咱們找不到結構的任何定義,那麼假如該結構是「繼承」類型,咱們會在上下文樹中指向父代的結構,這樣也能夠共享結構。若是是 reset 類型的結構,則會使用默認值。
若是最特殊的節點確實添加了值,那麼咱們須要另外進行一些計算,以便將這些值轉化成實際值。而後咱們將結果緩存在樹節點中,供子代使用。
若是某個元素與其同級元素都指向同一個樹節點,那麼它們就能夠共享整個樣式上下文。
讓咱們來看一個例子,假設咱們有以下 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>
爲了簡便起見,咱們只須要填充兩個結構:color 結構和 margin 結構。color 結構只包含一個成員(即「color」),而 margin 結構包含四條邊。
造成的規則樹以下圖所示(節點的標記方式爲「節點名 : 指向的規則序號」):
筆記:這裏的節點不是元素節點,就是指的規則樹中的節點(參考二叉樹的的定義,就是一種數據結構,這裏能夠理解爲CSS選擇器)。
假設咱們解析 HTML 時遇到了第二個 <div> 標記,咱們須要爲此節點建立樣式上下文,並填充其樣式結構。
通過規則匹配,咱們發現該 <div> 的匹配規則是第 一、2 和 6 條。這意味着規則樹中已有一條路徑可供咱們的元素使用,咱們只須要再爲其添加一個節點以匹配第 6 條規則(規則樹中的 F 節點)。
咱們將建立樣式上下文並將其放入上下文樹中。新的樣式上下文將指向規則樹中的 F 節點。
如今咱們須要填充樣式結構。首先要填充的是 margin 結構。因爲最後的規則節點 (F) 並無添加到 margin 結構,咱們須要上溯規則樹,直至找到在先前節點插入中計算過的緩存結構,而後使用該結構。咱們會在指定 margin 規則的最上層節點(即 B 節點)上找到該結構。
咱們已經有了 color 結構的定義,所以不能使用緩存的結構。因爲 color 有一個屬性,咱們無需上溯規則樹以填充其餘屬性。咱們將計算端值(將字符串轉化爲 RGB 等)並在此節點上緩存通過計算的結構。
第二個 <span> 元素處理起來更加簡單。咱們將匹配規則,最終發現它和以前的 span 同樣指向規則 G。因爲咱們找到了指向同一節點的同級,就能夠共享整個樣式上下文了,只需指向以前 span 的上下文便可。
對於包含了繼承自父代的規則的結構,緩存是在上下文樹中進行的(事實上 color 屬性是繼承的,可是 Firefox 將其視爲 reset 屬性,並緩存到規則樹上)。
例如,若是咱們在某個段落中添加 font 規則:
p {font-family:Verdana;font size:10px;font-weight:bold}
在 WebKit 中沒有規則樹,所以會對匹配的聲明遍歷 4 次。首先應用非重要高優先級的屬性(因爲做爲其餘屬性的依據而應首先應用的屬性,例如 display),接着是高優先級重要規則,而後是普通優先級非重要規則,最後是普通優先級重要規則。這意味着屢次出現的屬性會根據正確的層疊順序進行解析。最後出現的最終生效。
所以歸納來講,共享樣式對象(整個對象或者對象中的部分結構)能夠解決問題 1和問題 3。Firefox 規則樹還有助於按照正確的順序應用屬性。
樣式規則有一些來源:
p {color:blue}
<p style="color:blue" />
<p bgcolor="blue" />
後兩種很容易和元素進行匹配,由於元素擁有樣式屬性,並且 HTML 屬性可使用元素做爲鍵值進行映射。
咱們以前在第 2 個問題中提到過,CSS 規則匹配可能比較棘手。爲了解決這一難題,能夠對 CSS 規則進行一些處理,以便訪問。
樣式表解析完畢後,系統會根據選擇器將 CSS 規則添加到某個哈希表中。這些哈希表的選擇器各不相同,包括 ID、類名稱、標記名稱等,還有一種通用哈希表,適合不屬於上述類別的規則。若是選擇器是 ID,規則就會添加到 ID 表中;若是選擇器是類,規則就會添加到類表中,依此類推。
這種處理能夠大大簡化規則匹配。咱們無需查看每一條聲明,只要從哈希表中提取元素的相關規則便可。這種優化方法可排除掉 95% 以上規則,所以在匹配過程當中根本就不用考慮這些規則了 (4.1)。
咱們以以下的樣式規則爲例:
p.error {color:red} #messageDiv {height:50px} div {margin:5px}
<p class="error">an error occurred </p> <div id="messageDiv">this is a message</div>
咱們首先會爲 p 元素尋找匹配的規則。類表中有一個「error」鍵,在下面能夠找到「p.error」的規則。div 元素在 ID 表(鍵爲 ID)和標記表中有相關的規則。剩下的工做就是找出哪些根據鍵提取的規則是真正匹配的了。
例如,若是 div 的對應規則以下:
table div {margin:5px}
WebKit 和 Firefox 都進行了這一處理。
樣式對象具備與每一個可視化屬性一一對應的屬性(均爲 CSS 屬性但更爲通用)。若是某個屬性未由任何匹配規則所定義,那麼部分屬性就可由父代元素樣式對象繼承。其餘屬性具備默認值。
若是定義不止一個,就會出現問題,須要經過層疊順序來解決。
瀏覽器聲明是重要程度最低的,而用戶只有將該聲明標記爲「重要」才能夠替換網頁做者的聲明。一樣順序的聲明會根據特異性進行排序,而後再是其指定順序。HTML 可視化屬性會轉換成匹配的 CSS 聲明。它們被視爲低優先級的網頁做者規則。
選擇器的特異性由 CSS2 規範定義以下:
您使用的進製取決於上述類別中的最高計數。
例如,若是 a=14,您可使用十六進制。若是 a=17,那麼您須要使用十七進制;固然不太可能出現這種狀況,除非是存在以下的選擇器:html body div div p ...(在選擇器中出現了 17 個標記,這樣的可能性極低)。
一些示例:
* {} /* 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 */
找到匹配的規則以後,應根據級聯順序將其排序。WebKit 對於較小的列表會使用冒泡排序,而對較大的列表則使用歸併排序。對於如下規則,WebKit 經過替換「>」運算符來實現排序:
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 使用一個標記來表示是否全部的頂級樣式表(包括 @imports)均已加載完畢。若是在附加過程當中還沒有徹底加載樣式,則使用佔位符,並在文檔中進行標註,等樣式表加載完畢後再從新計算。
呈現器(renderer)在建立完成並添加到呈現樹時,並不包含位置和大小信息。計算這些值的過程稱爲佈局或重排(layout or reflow)。
HTML 採用基於流的佈局模型,這意味着大多數狀況下只要一次遍歷就能計算出幾何信息。處於流中靠後位置元素一般不會影響靠前位置元素的幾何特徵,所以佈局能夠按從左至右、從上至下的順序遍歷文檔。可是也有例外狀況,好比 HTML 表格的計算就須要不止一次的遍歷 (3.5)。
座標系是相對於根框架而創建的,使用的是上座標和左座標。
佈局是一個遞歸的過程。它從根呈現器(對應於 HTML 文檔的 <html>
元素)開始,而後遞歸遍歷部分或全部的框架層次結構,爲每個須要計算的呈現器計算幾何信息。
全部的呈現器都有一個「layout」或者「reflow」方法,每個呈現器都會調用其須要進行佈局的子代的 layout 方法。
筆記:佈局主要關注renderer(DOM中的元素節點或css中的盒子)的幾何尺寸及座標位置。
爲避免對全部細小更改都進行總體佈局,瀏覽器採用了一種「dirty 位」系統。若是某個呈現器發生了更改,或者將自身及其子代標註爲「dirty」,則須要進行佈局。
有兩種標記:「dirty」和「children are dirty」。「children are dirty」表示儘管呈現器自身沒有變化,但它至少有一個子代須要佈局。
全局佈局是指觸發了整個呈現樹範圍的佈局,觸發緣由可能包括:
佈局能夠採用增量方式,也就是隻對 dirty 呈現器進行佈局(這樣可能存在須要進行額外佈局的弊端)。
當呈現器爲 dirty 時,會異步觸發增量佈局。例如,當來自網絡的額外內容添加到 DOM 樹以後,新的呈現器附加到了呈現樹中。
筆記:對於作了dirty標記的renderer進行增量佈局,增量佈局不會觸發整個render tree的reflow。
佈局一般具備如下模式:
Firefox 使用「state」對象 (nsHTMLReflowState) 做爲佈局的參數(稱爲「reflow」),這其中包括了父呈現器的寬度。
Firefox 佈局的輸出爲「metrics」對象 (nsHTMLReflowMetrics),其包含計算得出的呈現器高度。
呈現器寬度是根據容器塊的寬度、呈現器樣式中的「width」屬性以及邊距和邊框計算得出的。
例如如下 div 的寬度:
<div style="width:30%"/>
clientWidth() - paddingLeft() - paddingRight()
clientWidth 和 clientHeight 表示一個對象的內部(除去邊框和滾動條)。
這些值會緩存起來,以用於須要佈局而寬度不變的狀況。
筆記:呈現器對應呈現樹(渲染樹)中的元素節點。每個呈現器都表明了一個矩形的區域,一般對應於相關節點的 CSS 框,這一點在 CSS2 規範中有所描述。它包含諸如寬度、高度和位置等幾何信息。
若是呈現器在佈局過程當中須要換行,會當即中止佈局,並告知其父代須要換行。父代會建立額外的呈現器(我理解爲匿名塊),並對其調用佈局。
在繪製階段,系統會遍歷呈現樹,並調用呈現器的「paint」方法,將呈現器的內容顯示在屏幕上。繪製工做是使用用戶界面基礎組件完成的。
筆記:繪製階段在佈局階段以後發生。
Firefox 對此過程進行了優化,也就是不添加隱藏的元素,例如被不透明元素徹底遮擋住的元素。
while (!mExiting) NS_ProcessNextEvent(thread);
根據 CSS2 規範,「畫布」這一術語是指「用來呈現格式化結構的空間」,也就是供瀏覽器繪製內容的區域。畫布的空間尺寸大小是無限的,可是瀏覽器會根據視口的尺寸選擇一個初始寬度。
根據 www.w3.org/TR/CSS2/zindex.html,畫布若是包含在其餘畫布內,就是透明的;不然會由瀏覽器指定一種顏色。
CSS 框模型描述的是針對文檔樹中的元素而生成,並根據可視化格式模型進行佈局的矩形框。
每一個框都有一個內容區域(例如文本、圖片等),還有可選的周圍補白、邊框和邊距區域。
每個節點都會生成 0..n 個這樣的框。
全部元素都有一個「display」屬性,決定了它們所對應生成的框類型。示例:
block - generates a block box. inline - generates one or more inline boxes. none - no box is generated.
默認值是 inline,可是瀏覽器樣式表設置了其餘默認值。例如,「div」元素的 display 屬性默認值是 block。
有三種定位方案:
定位方案是由「position」屬性和「float」屬性設置的。
框的佈局方式是由如下因素決定的:
block 框:造成一個 block,在瀏覽器窗口中擁有其本身的矩形區域。
inline 框:沒有本身的 block,可是位於容器 block 內。
block 採用的是一個接一個的垂直格式,而 inline 採用的是水平格式。
inline 框放置在行中或「行框」中。這些行至少和最高的框同樣高,還能夠更高,當框根據「底線」對齊時,這意味着元素的底部須要根據其餘框中非底部的位置對齊。若是容器的寬度不夠,inline 元素就會分爲多行放置。在段落中常常發生這種狀況。
相對定位:先按照普通方式定位,而後根據所需偏移量進行移動。
浮動框會移動到行的左邊或右邊。有趣的特徵在於,其餘框會浮動在它的周圍。下面這段 HTML 代碼:
<p> <img style="float:right" src="images/image.gif" width="100" height="100"> Lorem ipsum dolor sit amet, consectetuer... </p>
顯示效果以下:
這種佈局是準肯定義的,與普通流無關。元素不參與普通流。尺寸是相對於容器而言的。在固定定位中,容器就是可視區域。
這是由 z-index CSS 屬性指定的。它表明了框的第三個維度,也就是沿「z 軸」方向的位置。
這些框分散到多個堆棧(稱爲堆棧上下文)中。在每個堆棧中,會首先繪製後面的元素,而後在頂部繪製前面的元素,以便更靠近用戶。若是出現重疊,新繪製的元素就會覆蓋以前的元素。
堆棧是按照 z-index 屬性進行排序的。具備「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>
結果以下:
雖然紅色 div 在標記中的位置比綠色 div 靠前(按理應該在常規流程中優先繪製),可是 z-index 屬性的優先級更高,所以它移動到了根框所保持的堆棧中更靠前的位置。
個人理解,總結如下幾點:
一、瀏覽器根據輸入的URL向對應的服務器發送請求。
二、當服務器的響應返回後,瀏覽器開始加載(下載)返回的相應數據(HTML文檔)。
三、HTML文檔下載完畢後,瀏覽器的HTML解析器開始解析HTML文檔。
四、在解析HTML文檔的同時,預解析器(現代瀏覽器的優化,古老的瀏覽器沒有)會掃描文檔中引用的外部文件(css、js、圖片等)的URL,並解析之,而後下載他們(僅僅下載他們,預解析器不解析下載的文件,解析這些文件還要交給對應的解析器)。這些外部文件下載完畢以後,交給對應的解析器解析。
CSS解析器先將樣式表解析爲stylesheet對象(不是CSS規則樹),這一步不受任何其餘因素的影響。
js文件的下載不會阻塞其餘線程(HTML的解析,樹的構建等),但js代碼的執行就會阻塞其餘線程。
注意:js的執行不會阻塞CSS的解析,可是會阻塞HTML文檔的解析(即阻塞DOM樹的構建,前提是<script>的位置不在文檔的最下面),而CSS規則樹(Firefox中有,WebKit 中沒有)的構建要對照着DOM樹進行,而渲染樹的構建又依賴DOM樹和CSS規則樹,因此js的執行會阻塞渲染樹的構建。DOM樹、CSS規則樹和渲染樹的構建是同時進行的,不是說前一種樹所有構建完了才構建下一種樹。個人判斷,js的執行還會影響瀏覽器將頁面繪製在視口中(特別是alert語句,能夠經過setTimeout()來加深理解),這主要是由於瀏覽器繪製頁面的線程程與js代碼執行的線程是分開的,二者比較,後者的速度更快,效率更高。
五、瀏覽器一邊解析HTML文檔,一邊構建DOM樹,解析到哪裏,構建到哪裏。
我以爲
其餘相關文章:
一、應該知道的前端性能二三事 —— Reflow 和 Repaint
二、[ JS 進階 ] Repaint 、Reflow 的基本認識和優化 (2)
三、瀏覽器加載、解析、渲染的過程(這篇說的比較實在)