瀏覽器是如何解析html的?

iamswf發表於2018-12-23

當我們在瀏覽器位址列輸入一個合法的url時,瀏覽器首先進行DNS域名解析,拿到伺服器IP地址後,瀏覽器給伺服器傳送GET請求,等到伺服器正常返回後瀏覽器開始下載並解析html。這裡僅總結瀏覽器解析html的過程。

html頁面主要由domcssjavascript等部分構成,其中cssjavascript既能內聯也能以指令碼的形式引入,當然html中還可能引入imgiframe等其他資源。其實所有的這些資源也是以dom標籤的形式嵌入在html頁面中的,因此本篇總結說的html解析過程就是dom的解析過程。

1 dom解析過程

整個dom的解析過程是順序,並且漸進式的。

順序指的是從第一行開始,一行一行依次解析;漸進式則指得是瀏覽器會迫不及待的將解析完成的部分顯示出來,如果我們做下面這個實驗會發現,在斷點處第一個div已經在瀏覽器渲染出來了:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div>
        first div
    </div>
    <script>
        debugger
    </script>
    <div>
        second div
    </div>
</body>
</html>
複製程式碼

既然dom是從第一行按順序解析,那麼我們怎麼判斷dom何時解析完成呢?這個問題應該經常會在面試中問到,比如一般會問:

window.onloadDOMContentLoaded有什麼區別?

其實就是想看看是不是明白dom樹何時構建完成,這個問題確實很重要,尤其是對於幾年前的jquery技術棧來說,因為我們使用javascript操作dom或者給dom繫結事件有個前提條件就是需要dom樹已經建立完成。整個html頁面的dom解析完成時,dom樹也就構建完成了。dom樹構建完成後document物件會派發事件DOMContentLoaded來通知dom樹已構建完成。

html從第一行開始解析,遇到外聯資源(外聯css外聯javascriptimageiframe等)就會請求對應資源,那麼請求過程是否會阻塞dom的解析過程呢?答案是看情況,有的資源會,有的資源不會。下面按是否會阻塞頁面解析分為兩類:阻塞型非阻塞型,注意這裡區分兩類資源的標誌是document物件派發DOMContentLoaded事件的時間點,認為派發DOMContentLoaded事件才表示dom樹構建完成。

1.1 阻塞型

會阻塞dom解析的資源主要包括:

  • 內聯css
  • 內聯javascript
  • 外聯普通javascript
  • 外聯defer javascript
  • javascript標籤之前的外聯css

外聯javascript可以用asyncdefer標示,因此這裡分為了三類:外聯普通javascript外聯defer javascript外聯async javascript,這幾類外聯javascript本篇後面有詳細介紹。 dom解析過程中遇到外聯普通javascript會暫停解析,請求拿到javascript並執行,然後繼續解析dom樹

對於外聯defer javascript這裡重點說明下為什麼也歸於阻塞型。前面也說了,這裡以document物件派發DOMContentLoaded事件來標識dom樹構建完成,而defer javascript是在該事件派發之前請求並執行的,因此也歸類於阻塞型,但是需要知道,deferjavascript實際上是在dom樹構建完成與派發DOMContentLoaded事件之間請求並執行的,不過如果換個思路理解,<script>本身也是dom的一部分也就不難理解為什麼deferjavascript會在DOMContentLoaded派發之前執行了。

另外需要注意的是javascript標籤之前的外聯css。其實按說css資源是不應該阻塞dom樹的構建過程的,畢竟css隻影響dom樣式,不影響dom結構,MDN上也是這麼解釋的:

The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.

但是實際情況是dom樹的構建受javascript的阻塞,而javascript執行時又可能會使用類似Window.getComputedStyle()之類的API來獲取dom樣式,比如:

const para = document.querySelector('p');
const compStyles = window.getComputedStyle(para);
複製程式碼

因此瀏覽器一般會在遇到<script>標籤時將該標籤之前的外聯css請求並執行完成。但是注意這裡加了一個前提條件就是javascript標籤之前的外聯css,就是表示被javascript執行依賴的外聯css。這個容易忽略的點這篇文章也有說明,推薦閱讀。

這些阻塞型的資源請求並執行完之後dom樹的解析便完成了,這時document物件就會派發DOMContentLoaded事件,表示dom樹構建完成。

1.2 非阻塞型

不阻塞dom解析的資源主要包括:

  • javascript標籤之後的外聯css
  • image
  • iframe
  • 外聯async javascript

dom樹解析完成之後會派發DOMContentLoaded事件,對於外聯css資源來說分為兩類,一類是位於<script>標籤之前,一類是位於<script>標籤之後。位於<script>標籤之後的外聯css是不阻塞dom樹的解析的。外聯cssdom樹解析過程的影響這裡有一篇非常好的文章介紹:DOMContentLoaded and stylesheets,推薦閱讀。

DOMContentLoaded事件用來標識dom樹構建完成,那如何判斷另外這些非阻塞型的資源載入完成呢?答案是window.onload。由於該事件派發的過晚,因此一般情況下我們用不著,而更多的是用DOMContentLoaded來儘早的的操作dom

另外還有imageiframe以及外聯async javascript也不會阻塞dom樹的構建。這裡外聯async javascript又是什麼呢?下一節整體介紹下外聯javascript

2 外聯javascript載入過程

html頁面中可以引入內聯javascript,也可以引入外聯javascript外聯javascript又分為:

  • 外聯普通javascript
<script src="indx.js"></script>
複製程式碼
  • 外聯defer javascript
<script defer src="indx.js"></script>
複製程式碼
  • 外聯async javascript
<script async src="indx.js"></script>
複製程式碼

其中第一種就是外聯普通javascript,會阻塞html的解析,html解析過程中每遇到這種<script>標籤就會請求並執行,如下圖所示,綠色表示html解析;灰色表示html解析暫停;藍色表示外聯javascript載入;粉色表示javascript執行

標記
外聯普通javascript的載入執行過程如下:
外聯普通javascript
第二種外聯defer javascript稍有不同,html解析過程中遇到此類<script>標籤不阻塞解析,而是會暫存到一個佇列中,等整個html解析完成後再按佇列的順序請求並執行javascript,但是這種外聯defer javascript全部載入並執行完成後才會派發DOMContentLoaded事件,外聯defer javascript的載入執行過程如下:
外聯defer javascript
第三種外聯async javascript則不阻塞html的解析過程,注意這裡是說的指令碼的下載過程不阻塞html解析,如果下載完成後html還沒解析完成,則會暫停html解析,先執行完成下載後的javascript程式碼再繼續解析html,過程如下:
外聯async javascript
但是如果html已經解析完畢,外聯async javascript還未下載完成,則不阻塞DOMContentLoaded事件的派發。因此外聯async javascript很有可能來不及監聽DOMContentLoaded事件,比如stackoverflow上的這個問題

說明下,這幾個圖引用自這裡

3 DOMContentLoaded相容性問題

DOMContentLoaded最開始由firefox提出,其他瀏覽器覺得非常有用也相繼開始支援,但是特性卻稍有不同,比如operajavascript的執行並不等待外聯css的載入。直到HTML5出來後將DOMContentLoaded標準化,依照HTML5標準,javascript指令碼執行前,出現在當前<script>之前的<link rel="stylesheet">必須完全載入。

那麼在所有瀏覽器標準化之前怎麼解決DOMContentLoaded的相容性問題呢?可以參考jQuery.ready()方法的實現,對於該方法的原始碼分析網上已經一大堆了,這裡就不做分析了,直接說下原理。其實是就是用了MDN: DOMContentLoaded中介紹的相容性方法,ie9才開始支援DOMContentedLoadedie8環境可以通過檢測document.readystate狀態來確認dom樹是否構建完成。document.readystate包括3種狀態:

  • loading - html文件載入中
  • interactive - html文件載入並解析完成,但是圖片等資源還未完成載入,相當於DOMContentLoaded
  • complete - 所有資源載入完成,相當於window onload

因此我們通過判斷document.readystate的狀態為interactive來模擬DOMContentLoaded時間點。但是這裡需要注意一點,以.ready()方法為例,我們可能在下面這幾個地方呼叫:

  • 內聯javasctipt
  • 外聯普通javascript
  • 外聯defer javascript
  • 外聯async javascript

其中3三個地方直接判斷document.readystate肯定是loading狀態,只有外聯async javascript可能出現document.readystateinteractivecompleted的狀態,因為外聯async javascript是不阻塞dom解析的,因此為了完全覆蓋前面的4種情況,需要監聽document.readystate的變化:

if (document.readystate === 'interactive'
    || document.readystate === 'complete') {
        // 呼叫ready回撥函式
} else {
    document.onreadystatechange = function () {
        if (document.readystate === 'interative') {
            // 呼叫ready回撥函式
        }
    } 
}
複製程式碼

4 引用

主要參考了以下文章,推薦閱讀:

  1. Page lifecycle: DOMContentLoaded, load, beforeunload, unload
  2. DOMContentLoaded and stylesheets
  3. script標籤: async vs defer attributes
  4. MDN: DOMContentLoaded
  5. MDN: readystatechange
  6. Replace jQuery’s Ready() with Plain JavaScript

相關文章