微信公衆號:愛寫bugger的阿拉斯加
若有問題或建議,請後臺留言,我會盡力解決你的問題。
前言
此文章是我最近在看的【WebKit 技術內幕】一書的一些理解和作的筆記。
而【WebKit 技術內幕】是基於 WebKit 的 Chromium 項目的講解。html
書接上文 瀏覽器內核之資源加載與網絡棧程序員
本文介紹 W3C 的 DOM 模型以後,深刻 WebKit 的核心部分,剖析 WebKit 的 HTML 解釋器是如何將從網絡或者本地文件獲取的字節流轉成內部表示的結構 --- DOM 樹。web
1. DOM 模型
1.1.1 DOM 標準
DOM (Document Object Model)的全稱是文檔對象模型,它能夠以一種獨立於平臺和語言的方式訪問和修改一個文檔的內容和結構。這裏的文檔能夠是 HTML 文檔、XML 文檔或者 XHTML 文檔。DOM 以面向對象的方式來描述文檔,在 HTML 文檔中,Web 開發者可使用 JavaScript 語言來訪問、建立、刪除或者修改 DOM 結構,其主要目的是動態改變 HTML 文檔的結構。算法
使用 DOM 表示的文檔被描述成一個樹形結構,使用 DOM 的接口能夠對 DOM 樹結構進行操做。瀏覽器
每一級的版本都對之前的版本進行了補充並伴隨新功能的加入,每一個版本都對 DOM 的不一樣部分進行了定義。安全
1.1.2 DOM 樹
1.1.2.1 結構模型
DOM 結構構成的基本要素是 「節點」 ,而文檔的 DOM 結構就是由層次化的節點組成。在 DOM 模型中,節點的概念很寬泛,整個文檔(Document )就是一個節點,稱爲文檔節點。HTML 中的標記(Tag)也是一種節點,稱爲元素(Element)節點。還有一些其餘類型的節點,例如 屬性節點(標記的屬性)、Entity 節點、ProcessingIntruction 節點、CDataSection 節點、註釋(Comment)節點等。微信
1.1.2.2 DOM 樹
衆多的節點按照層次組織構成一個 DOM 樹結構。
如圖 5 - 4 網絡
DOM 樹的根就是 HTMLDocument , HTML 網頁中的標籤則被轉換成一個個的元素節點。同數據結構中的樹形結構同樣,這些節點之間也存在父子或兄弟關係。數據結構
1.2 HTML 解釋器
1.2.1 解釋過程
HTML 解釋器的工做就是將網絡或者本地磁盤獲取的 HTML 網頁和資源從字節流解釋成 DOM 樹結構。這一過程大體能夠理解成圖 5-5所述的步驟。併發
這過程當中,WebKit 內部對網頁內容在各個階段的結構表示。 WebKit 中這一過程以下:首先是字節流,通過解碼以後是字符流,而後經過詞法分析器會被解釋成詞語(Tokens),以後通過語法分析器構建成節點,最後這些節點被組建成一棵 DOM 樹。
1.2.2 詞法分析
在進行詞法分析以前,解釋器首先要作的事情就是檢查該網頁內容使用的編碼格式,以便後面使用合適的解碼器。若是解釋器在 HTML 網頁中找到了設置的編碼格式, WebKit 會使用相應的解碼器來將字節流轉換成特定格式的字符串。若是沒有特殊格式,詞法分析器 HTMLTokenizer 類能夠直接進行詞法分析。
詞法分析的工做都是由 HTMLTokenizer 來完成 ,簡單來講,它就是一個狀態機---輸入的是字符串,輸出的是一個個詞語。由於字節流多是分段的,因此輸入的字符串可能也是分段的,可是這對詞法分析器來講沒有什麼特別之處,它會本身維護內部的狀態信息。
詞法分析器的主要接口是 「nextToken」 函數,調用者只須要關鍵字符串傳入,而後就會獲得一個詞語,並對傳入的字符串設置相應的信息,表示當前處理完的位置,如此循環,若是詞法分析器遇到錯誤,則報告狀態錯誤碼,主要邏輯在圖 5-8 中給予了描述。
對於 「nextToken」 函數的調用者而言,它首先設置輸入須要解釋的字符串,而後循環調用 NextToken 函數,直處處理結束。 「nextToken」 方法每次輸出一個詞語,同時會標記輸入的字符串,代表哪些字符已經被處理過了。所以,每次詞法分析器都會根據上次設置的內部狀態和上次處理以後的字符串來生成一個新的詞語。 「nextToken」 函數內部使用了超過 70 種狀態,圖中只顯示了 3 種狀態。對於每一個不一樣的狀態,都有相應的處理邏輯。
1.2.3 XSSAuditor 驗證詞語
當詞語生成以後,WebKit 須要使用 XSSAuditor 來驗證詞語流(Token Stream)。XSS 指的是 Cross Site Security , 主要是針對安全方面的考慮。
根據 XSS 的安全機制,對於解析出來的這些詞語,可能會阻礙某些內容的進一步執行,因此 XSSAuditor 類主要負責過濾這些被阻止的內容,只有經過的詞語纔會做後面的處理。
1.2.4 詞語到節點
通過詞法分析器解釋以後的詞語隨之被 XSSAuditor 過濾而且在沒有被阻止以後,將被 WebKit 用來構建 DOM 節點。從詞語到構建節點的步驟是由 HTMLDocumentParser 類調用 HTMLTreeBuilder 類的 「constructTree」 函數來實現。
1.2.5 節點到 DOM 樹
從節點到構建 DOM 樹,包括爲樹中的元素節點建立屬性節點等工做由 HTMLConstructionSite 類來完成。正如前面介紹的,該類包含一個 DOM 樹的根節點 ——HTMLDocument 對象,其餘的元素節點都是它的後代。
由於 HTML 文檔的 Tag 標籤是有開始和結束標記的,因此構建這一過程可使用棧結構來幫忙。HTMLConstructionSite 類中包含一個 「HTMLElementStack」 變量,它是一個保存元素節點的棧,其中的元素節點是當前有開始標記可是尚未結束標記的元素節點。想象一下 HTML 文檔的特色,例如一個片斷 「<body><div><img></img></div></body>」,當解釋到 img 元素的開始標記時,棧中的元素就是 body 、div 和 img ,當遇到 img 的結束標記時,img 退棧, img 是 div 元素的子女;當遇到 div 的結束標記時,div 退棧,代表 div 和它的子女都已處理完,以此類推。
同 DOM 標準同樣,一切的基礎都是 Node 類。在 WebKit 中, DOM 中的接口 Interface 對應於 C++ 的類,Node 類是其餘類的基類,圖 5-10 顯示了 DOM 的主要相關節點類。圖中的 Node 類實際上繼承自 EventTarget 類,它代表 Node 類可以接受事件,這個會在 DOM 事件處理中介紹。Node 類還繼承自另一個基類 ——ScriptWrappable,這個跟 JavaScript 引擎相關。
Node 的子類就是 DOM 中定義的同名接口,元素類,文檔類和屬性類均繼承自一個抽象出來的 ContainerNode 類,代表它們可以包含其餘的節點對象。回到 HTML 文檔來講,元素和文檔對應的類注是 HTMLElement 類和 HTMLDocument 類,實際上 HTML 規範還包含衆多的 HTMLElement 子類,用於表示 HTML 語法中衆多的標籤。
1.2.6 網頁基礎設施
上面介紹了 Frame 、Document 等 WebKit 中的基礎類,這些都是網頁內部的概念,實際上,WebKit 提供了更高層次的設施,用於表示整個網頁的一些類,WebKit 中的 接口部分 就是基於它們來提供的,表示網頁的類既提供了構建 DOM 樹等操做,同時也提供了接口用於佈局。渲染等操做。
1.2.7 線程化的解釋器
在 Renderer 進程中有一個線程,該線程用來處理 HTML 文檔的解釋任務,在 HTML 解釋器的步驟中,WebKit 的 Chromium 移植跟其餘的 WebKit 移植也存在不一樣之處。
線程化的解釋器就是利用單獨的線程來解釋 HTML 文檔。由於在WebKit 中,網絡資源的字節流自 IO 線程傳遞給渲染線程以後,後面的解釋、佈局和渲染等工做基本上就是工做在該線程,也就是渲染線程完成的(這不是絕對的)。由於 DOM 樹只能在渲染線程上建立和訪問,這也就是說構建 DOM 樹的過程只能在渲染線程中進行。可是,從字符到詞語這個階段能夠交給單獨的線程來作,Chromium 瀏覽器使用的就是這個思想。
具體的實現過程:
字符串 (傳給)=> HTMLDocumentParser類 (建立一個新的對象)=> BackgroundHTMLParser 來負責處理 (交給)=> 前一步建立的對象
WebKit 會檢查是否須要建立用於解釋字符串的線程 HTMLParserThread 。若是該線程已存在,WebKit 就將剛剛的任務傳遞給這一新線程, 圖 5-13 描述了這一過程。
在 HTMLParserThread 線程中,WebKit 所作的事情包括將字符串解釋成一個個詞語,而後使用以前提到的 XSSAuditor 進行安全檢查。這是在一個新的線程中執行。主要區別在於解釋成詞語以後,WebKit 會分批次地將結果詞語傳遞給渲染線程。
1.2.8 JavaScript 的執行
在 HTML 解釋器的工做過程當中,可能會有 JavaScript 代碼(全局做用域的代碼)須要執行,它發生在將字符串解釋成詞語以後、建立各類節點的時候。這也是全局執行的 JavaScript 代碼不能訪問 DOM 樹的緣由——由於 DOM 樹尚未被建立完。
因此建議 JavaScript 的使用以下:
一、將 「script」 元素加上 「async」 屬性,代表這是一個能夠異步執行的 JavaScript 代碼。
二、將 「script」 元素放在 「body」 元素的最後,這樣它不會阻礙其餘資源的併發下載。
可是不這樣作的時候,WebKit 使用預掃描和預加載機制來實現資源的併發下載而不被 JavaScript 的執行所阻礙。
具體作法是:當遇到須要執行 JavaScript 代碼的時候,WebKit 先暫停當前 JavaScript 代碼的執行,使用預先掃描器 HTMLPreloadScanner 類來掃描後面的詞語。若是 WebKit 發現它們須要使用其餘資源,那麼使用預資源加載器 HTMLPreloadScanner 類來發送請求,在這以後,才執行 JavaScript代碼。預先掃描器自己並不建立節點對象,也不會構建 DOM 樹,因此速度比較快。
當 DOM 樹構建完以後,WebKit 觸發 「DOMContentLoaded」 事件,註冊在該事件上的 JavaScript 函數會被調用。當所在資源都被加載完以後,WebKit 觸發 「onload」 事件。
WebKit 將 DOM 樹建立過程當中須要執行的 JavaScript 代碼交由 HTMLScriptRunner 類來負責。工做方式很簡單,就是利用 JavaScript 引擎來執行 Node 節點中包含的代碼,具體能夠參考 「HTMLScriptRunner::executeParsingBlockingScript」 方法。
1.3 DOM 事件機制
1.3.1 事件的工做過程
事件在工做過程當中使用兩個主體,第一個是事件(event),第二個是事件目標(EventTarget)。WebKit 中用 EventTarget 類來表示 DOM 規範中 Events 部分定義的事件目標。
每一個 事件都有屬性來標記該事件的事件目標。當事件到達事件目標(如一個元素節點)的時候,在這個目標上註冊的監聽者(Event Listeners)都會有觸發調用,而這些監聽者的調用順序不是固定的,因此不能依賴監聽者註冊的順序來決定你的代碼邏輯。
圖 5-17 是 EventTarget 接口的定義。圖中的接口是用來註冊和移除監聽者的。
事件處理最重要就是事件捕獲(Event capture)和事件冒泡(Event bubbling)這兩種機制。圖 5-18 是事件捕獲和事件冒泡的過程。
當渲染引擎接收到一個事件的時候,它會經過 HitTest(WebKit 中的一種檢查觸發gkwrd哪一個區域的算法)檢查哪一個元素是直接的事件目標。在圖 5-18 中,以 「img」 爲例,假設它是事件的直接目標,這樣,事件會通過自頂向下和自底向上的兩個過程。
事件的捕獲是自頂向下,事件先是到 document 節點,而後一路到達目標節點。在圖 5-18 中,順序就是 「#document」 -> "HTML" -> "body" -> "img" 這樣一個順序。事件能夠在這一傳遞過程當中被捕獲,只須要在註冊監聽者的時候設置相應參數便可。默認狀況下,其餘節點不捕獲這樣的事件。若是網頁註冊了這樣的監聽者,那麼監聽者的回調函數會被調用,函數能夠經過事件的 「stopPropagation」 函數來阻止事件向下傳遞。
事件的冒泡過程是從下向上的順序,它的默認行爲是不冒泡,可是是事件包含一個是否冒泡的屬性。當這一屬性爲真的時候,渲染引擎會將該事件首先傳遞給事件的目標節點的父親,而後是父親的父親,以此類推。同捕獲動做同樣,這此監聽函數也可使用 「stopPropagation」 函數來阻止事件向上傳遞。
1.3.2 WebKit 的事件處理機制
DOM 的事件分爲不少種,與用戶相關的只是其中的一種,稱爲 UIEvent ,其餘的包括 CustomEvent、MutationEvent 等。UIEvent 又能夠分爲不少種,包括可是不限於 FocusEvent、MouseEvent、KeyboardEvent、Composition 等。
基於 WebKit 的瀏覽器事件處理過程,首先是作 HitTest ,查找事件發生處的元素,檢查該元素有無監聽者。若是網頁的相關節點註冊了事件的監聽者,那麼瀏覽器會把事件派發給 WebKit 內核來處理。同時,瀏覽器也可能須要理解和處理這樣的事件。這主要是由於,有些事件瀏覽器必須響應從而對網頁做默認處理。
EventHandler 類是處理事件的核心類,它除了須要將各類事件傳給 JavaScript 引擎以調用響應的監聽者以外,它還會識別鼠標事件,來觸發調用右鍵菜單、拖放效果等與事件密切相關的工做,並且 EventHandler 類還支持網頁的多框結構。EventHandler 類的接口比較容易理解,可是它的處理邏輯極其複雜。
圖 5-20 簡單描述了鼠標事件的調用過程,這一過程自己是比較簡單的,複雜之處在於 WebKit 的 EventHandler 類。
WebKit 中還有些跟事件處理相關的其餘類,例如 EventPathWalker、EventDispatcher 類等,這些類都是爲了解決事件在 DOM 樹中傳遞的問題。
1.4 影子(Shadow)DOM
影子 DOM 是一個新東西,主要解決了一個文檔中可能須要大量交互的多個 DOM 樹創建和維護各自的功能邊界的問題。
1.4.1 什麼是影子 DOM
當開發這樣一個用戶界面的控件——這個控件可能由一些 HTML 的標籤元素組成,這些元素能夠組成一顆 DOM 樹的子樹。這樣一個 HTML 控件能夠被處處使用,可是問題隨之而來,那就是每一個使用控件的地方都會知道這個子樹的結構。
當網頁的開發者須要訪問網頁 DOM 樹的時候,這些控件內部的 DOM 子樹都會暴露出來,這些暴露的節點不只可能給 DOM 樹的遍歷帶來不少麻煩,並且也可能給 CSS 的樣式選擇帶來問題,由於選擇器無心中可能會改變這些內部節點的樣式,從而致使很奇怪的控件界面。
如何將內部的節點信息封裝起來,就像 C++ 語言的類同樣,同時又可以將這些節點渲染出來呢 ? W3C 工做組提出的影子 DOM 概念。影子 DOM 的規範草案可以使得一些 DOM 節點在特定範圍內可見,而在網頁的 DOM 樹中卻不可見,可是網頁渲染的結果中包含了這些節點,這就使得封裝變得容易不少。
圖 5-21 描述了 HTML 文檔對應的 DOM 樹和 「div」 元素包含的一個影子 DOM 子樹。當使用 JavaScript 代碼訪問 HTML 文檔的 DOM 樹的時候,一般的接口是不能直接訪問到影子 DOM 子樹中的節點的,JavaScript 代碼只能經過特殊的接口方式。
HTML5 支持了不少新的特性,例如對視頻、音頻的支持,讀者會發現這些元素實際上是由很複雜的控制界面組成,這些界面也是使用 HTML 元素編寫,可是在 DOM 樹中,你沒法找到相應的節點,這其實也是使用了影子 DOM 的思想。
由於影子 DOM 的子樹在整個網頁的 DOM 樹中不可見,那麼事件是如何處理的呢 ?事件中須要包含事件目標,這個目標固然不能是不可見的 DOM 節點,因此事件目標其實就是包含影子 DOM 子樹的節點對象。事件捕獲的邏輯沒有發生變化,在影子 DOM 子樹內也會繼續傳遞。當影子 DOM 子樹中的事件向上冒泡的時候, WebKit 會同時向整個文檔的 DOM 上傳遞該事件,以免一些很奇怪的行爲。
1.4.2 WebKit 的支持
WebKit 已經支持影子 DOM 的規範草案,雖然還存在一些問題。支持影子 DOM 的相關類在目錄 「Source/core/dom/shadow」 下,裏面的主要類是 ShadowRoot ,表示的是影子 DOM 的根節點。ShadowRoot 類繼承自 DocumentFragment 類,因此它一樣有 Node 節點的屬性和方法,於是在影子 DOM 樹的內部,遍歷樹沒有什麼特別不一樣的地方。
當遍歷 HTML 文檔對應 DOM 樹的時候,WebKit 須要作特別的判斷,因此讀者會發如今 WebKit 的 Node 類實現中存在大量的條件語句,用來檢查當前節點是不是 ShadowRoot 對象,若是是該類的對象,把它做爲不一樣 DOM 樹之間的邊界。有時候 WebKit 還須要對 ShadowRoot 對象做出特別處理,好比某些狀況會略過它的子樹,一樣的,在事件處理的支持類 EventPathWalker 和 EventRetargeter 中,也須要作一些特別的處理邏輯,原理就是上面所述,細節再也不介紹。
1.4.3 實踐:使用影子 DOM
示例代碼 5-2 給出了一個簡單的使用 webkitCreateShadowRoot 接口來建立影子 DOM 子樹的例子。網頁只包含了一個 「div」 元素,JavaScript 代碼使用該元素建立了一個影子 DOM 子樹的根節點,而後該根節點下加入了兩個子女,第一個是圖片元素,第二個是 「div」 元素,該元素內部包含了一些文本。
讀者能夠打開 Chrom 瀏覽器的開發者工具,而後打開控制檯,在其中輸入 「document.firstChild.firstChild.nextElementSibling.firstElementChild.firstElementChild」 後會發現結果是空的,根據對應關係 「#document-> html -> head -> body -> div -> null」,雖然網頁中沒有 ‘head’ 元素,可是 DOM 樹仍然會建立該節點。同時讀者會發現 「div」 元素沒有子女,影子 DOM 子樹真的被隱藏起來了,成爲真正的影子。
最後
但願本文對你有點幫助。
下期分享 第六章 CSS 解釋器和樣式佈局 敬請期待。
對 全棧開發 有興趣的朋友能夠掃下方二維碼關注個人公衆號 —— 愛寫bugger的阿拉斯加
分享 web 開發相關的技術文章,熱點資源,全棧程序員的成長之路。