原文連結:Tags to DOM
原文作者:Travis Leithead
譯者:wangds
這是一個系列文章,分為四個部分介紹了從輸入URL到頁面呈現的詳細過程
- 客戶端從伺服器獲取資源(Server to Client)點選檢視
- 標籤轉化成DOM的過程(tags to DOM)
- CSS解析的過程(braces to pixels)
- 編譯執行javascript的過程(var to JIT)
本篇翻譯的是第二部分:Tags to DOM
在上一篇文章中(客戶端從伺服器獲取資源),我們談論了資源是如何從伺服器到達客戶端的,同時也闡述了一些關於快取、同源的概念。本篇文章將聊一聊HTML
資源是如何轉化成DOM tree
的。
解析的過程(Parsing)
當瀏覽器獲得了資源以後要進行的第一步工作就是解析,整個解析的過程可以拆分成如下四個步驟:
- 解碼(
encoding
) - 預解析(
pre-parsing
) - 符號化(
Tokenization
) - 構建樹(
tree construction
)
1.解碼(encoding
)
客戶端接收的內容可以是HTML
文字、影象等等任何格式的資料。在從伺服器到客戶端展示的過程中,資料經過了如下的過程:
- 伺服器將資料轉換成
bit
bit
進行資料傳輸- 客戶端將
bit
轉化成資料
所以客戶端必須知道資料轉化成bit
的格式,以便客戶端將bit
反轉成正確的資料。
我們以HTML
舉例來說,HTML
的文字格式有很多種,所以伺服器在返回資料的時候在響應頭中使用Content-Type
告知瀏覽器文字的內容型別,同時在文字檔案頭部使用Byte Order Mark
告知瀏覽器編碼格式。如果瀏覽器仍然判斷不了解碼方式,瀏覽器還會自行匹配一種解碼格式來處理資料。有時候,解碼格式也會寫在<meta>
標籤中。這就導致會出現一種奇葩的現象,瀏覽器先是以自己的方式去解碼文字,然後遇到了標明解碼方式的<meta>
標籤。這時候瀏覽器就會重新解析按照<meta>
標籤進行解析。
我們現在經常在HTML
中使用的檔案格式是UTF-8
,那是因為UTF-8
能較完整的支援Unicode
字元範圍,同時與CSS
、JavaScript
中常見的節字元具有良好的ASCII
相容性。一般瀏覽器預設的解碼格式也是
UTF-8
。當解碼出錯的時候,我們會看到螢幕上全部都是亂碼字元。
2.預解析(Pre-parsing/scanning)
完成了解碼以後,瀏覽器會開啟一個預解析的過程,“偷偷獲取”後面需要載入的資源,減少處理的時間。預解析的過程不是一個完整的解析過程。舉例來說,預解析不會處理HTML
內元素之間巢狀的父子層級。但是預解析還是能識別一些特殊的HTML
標籤、元素屬性值以及URL
。假如我們的頁面中有個img
(如下),預解析就會注意src
屬性值,並將獲取這個圖片的請求加到請求佇列中,儘早的將圖片獲取到本地以便顯示。我們也可以通過prefetch
和preload
兩種屬性將需要儘早載入的資源告知於瀏覽器。
<img src="https://somewhere.example.com/images/dog.png" alt="">
複製程式碼
3.符號化(TOKENIZATION)
符號化是將輸入解析成為符號,像開始標籤(<
)、結束標籤(>
)、註釋(//
)、文字內容等等。然後這些符號將被送到解析的下一個步驟。完成符號化工作的機器叫做符號識別器( tokenizer
),符號識別器是一種狀態機。
我們以<video controls>
的標籤為例:
- 狀態機初始狀態是
Data State
, - 當遇到
<
的時候,狀態變成Tag open state
- 當解析到字母v的時候,狀態變成
Tag name state
- 當解析到字母c的時候,狀態變成
in attribute name state
- 當解析完字母s的時候,狀態變成
after attribute name state
- 當解析完
>
的時候,狀態變成Data state
從解析<video controls>
標籤我們就可以知道,整個頁面會碰到很多標籤,符號識別器就會不斷的重複狀態變化。(下圖中的箭頭就是狀態機處於的某個位置)
HTML標準
目前為符號識別器定義了80個獨立的狀態。在解析的過程中,符號識別器可以將任何的文字轉化成HTML
文件(即便文字內容不是有效的HTML
)。這種彈性的機制使得HTML
更加的易於開發,但是也可能導致因為寫錯標籤出現特殊的bug。
對於那些更喜歡使用‘非黑即白’驗證規則的人,瀏覽器內建了一種替代解析機制,可將任何解析錯誤視為災難性的故障(出現錯誤就會導致無法呈現內容)。 這種解析模式使用XML
規則來處理HTML
,我們可以通過設定MIME
型別的值為application / xhtml + xml
開啟這種解析模式。
瀏覽器可以同時處理預解析和標記化兩個過程(也就是說這是兩個執行緒)。
4. 構建樹(tree construction
)
在上一步符號化以後,解析器獲得這些符號標記,然後以合適的方法建立DOM
物件並將這些符號插入到DOM
物件中。DOM
物件的資料結構是樹狀的,所以這個過程稱為構造樹(tree construction
)。順便說一句,在IE
的歷史中,大部分時間裡沒有使用樹結構。
另外現代瀏覽器為了相容舊版本HTML
,解析的過程還是十分複雜的。例如瀏覽器可以自動閉合標籤。
<p>sincerely<p>The authors</p> //HTML的樣子
<p>sincerely</p><p>The authors</p>//瀏覽器解析出來的樣子
複製程式碼
我們可以使用JavaScript
去任意修改DOM
。假如我們用JavaScript
進行無意義的操作(例如在video
元素內插入一個table
單元格),渲染引擎可以甄別出這些操作不合規,也不進行渲染(但是這個過程渲染引擎也進行工作只是不展示)。
因為JavaScript
可以在DOM
中新增內容。所以遇到JavaScript
檔案的時候,DOM
將停止解析;如果JavaScript
檔案內呼叫了document.write
API,解析器將重新開始解析過程。
事件(Events)
當整個解析的過程完成以後,瀏覽器會通過DOMContentLoaded
事件來通知DOM
解析完成。事件相當於一套廣播系統用來通知和監聽瀏覽器。除了DOMContentLoaded
事件,還有load
事件(表示所有資源已經載入完成,包括圖片、視訊、CSS
等等)、unload
事件表示介面即將關閉、滑鼠事件鍵盤事件等等。
瀏覽器在DOM
物件中還會建立一個事件物件,該物件包含大量有用的資訊(如螢幕中的座標、按下的按鍵等等)。當JavaScript
觸發事件的時候,就會同時產生事件物件。
在目標元素上觸發事件的時候,需要從DOM
樹的根元素開始向子元素查詢,這個過程俗稱事件捕捉階段。到達目標元素以後,還要逐級向上返回到根元素上,這個過程俗稱事件冒泡階段。
事件是可以進行關閉的,我們可以在事件捕獲或者傳播的過程中選擇關閉,例如在form
的提交過程中判斷字串為空則關閉提交。
DOM
解析器解析出來的元素可以包含一些基本的互動功能(<video>
能播放視訊、<button>
按鈕等),但是這些基本的互動功能還不能滿足我們的需求,我們需要使用css
和JavaScript
將頁面展示的更加豐富多彩,而DOM
就是HTML
元素與其他資源互動的橋樑。
元素的介面
在解析器將元素放入DOM
樹之前,解析器會根據不同元素的名稱賦予元素不同的介面功能。
下面是一些通用的功能:
- 可以訪問元素內的子元素集合
- 可以搜尋子元素的屬性和方法
- 最重要的是,可以不通過解析器新增新的元素到
DOM
中
像<table>
元素具有查詢刪除表內所有單元格的能力;<canvas>
具有描繪線條、圖形的能力。但是想要呼叫這個介面還是需要JavaScript
來操作。
每當我們使用JavaScript
操作DOM
的時候,將會觸發瀏覽器的一些連鎖反應,這些反應是為了讓更改後的頁面更快的呈現在螢幕上。例如:
- 用數字代表通用的元素名稱和屬性,瀏覽器用使用雜湊表進行快速識別這些數字
- 將頻繁變更的子元素進行快取,方便子元素快速迭代
- 將
sub-tree
的跟蹤變化降到最低,避免‘汙染’整個DOM
樹
DOM
中的HTML
元素是頁面展示的基礎,DOM
的能力也不僅僅侷限於上面我們提到的功能。其他諸如訪問儲存系統(瀏覽器中的快取)、裝置功能(藍芽、定位)、以及3D圖形繪製等等功能。隨著瀏覽器的不斷進步和web標準的不斷實施,DOM
的功能只會越來越強大,本文只涉及了一部分而已。
結束語
以上就包含了標籤轉化成DOM的過程。下一篇文章我們將聊聊CSS解析的過程。
限於本人水平有限,文中如有錯誤感謝您指正。