當我們在瀏覽器位址列輸入一個合法的url
時,瀏覽器首先進行DNS
域名解析,拿到伺服器IP地址後,瀏覽器給伺服器傳送GET
請求,等到伺服器正常返回後瀏覽器開始下載並解析html
。這裡僅總結瀏覽器解析html的過程。
html
頁面主要由dom
、css
、javascript
等部分構成,其中css
和javascript
既能內聯
也能以指令碼
的形式引入,當然html
中還可能引入img
、iframe
等其他資源。其實所有的這些資源也是以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.onload
和DOMContentLoaded
有什麼區別?
其實就是想看看是不是明白dom樹
何時構建完成,這個問題確實很重要,尤其是對於幾年前的jquery
技術棧來說,因為我們使用javascript
操作dom
或者給dom
繫結事件有個前提條件就是需要dom樹
已經建立完成。整個html
頁面的dom
解析完成時,dom樹
也就構建完成了。dom樹構建完成後document
物件會派發事件DOMContentLoaded
來通知dom樹
已構建完成。
html
從第一行開始解析,遇到外聯
資源(外聯css
、外聯javascript
、image
、iframe
等)就會請求對應資源,那麼請求過程是否會阻塞dom
的解析過程呢?答案是看情況,有的資源會,有的資源不會。下面按是否會阻塞頁面解析分為兩類:阻塞型
與非阻塞型
,注意這裡區分兩類資源的標誌是document
物件派發DOMContentLoaded
事件的時間點,認為派發DOMContentLoaded
事件才表示dom樹
構建完成。
1.1 阻塞型
會阻塞dom
解析的資源主要包括:
- 內聯css
- 內聯javascript
- 外聯普通javascript
- 外聯defer javascript
- javascript標籤之前的外聯css
外聯javascript
可以用async
與defer
標示,因此這裡分為了三類:外聯普通javascript
,外聯defer javascript
、外聯async javascript
,這幾類外聯javascript
本篇後面有詳細介紹。
dom
解析過程中遇到外聯普通javascript
會暫停解析,請求拿到javascript
並執行,然後繼續解析dom樹
。
對於外聯defer javascript
這裡重點說明下為什麼也歸於阻塞型
。前面也說了,這裡以document
物件派發DOMContentLoaded
事件來標識dom樹
構建完成,而defer javascript
是在該事件派發之前請求並執行的,因此也歸類於阻塞型,但是需要知道,defer
的javascript
實際上是在dom樹
構建完成與派發DOMContentLoaded
事件之間請求並執行的,不過如果換個思路理解,<script>
本身也是dom
的一部分也就不難理解為什麼defer
的javascript
會在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 forstylesheets
, 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樹
的解析的。外聯css
對dom樹
解析過程的影響這裡有一篇非常好的文章介紹:DOMContentLoaded and stylesheets,推薦閱讀。
DOMContentLoaded
事件用來標識dom樹
構建完成,那如何判斷另外這些非阻塞型
的資源載入完成呢?答案是window.onload
。由於該事件派發的過晚,因此一般情況下我們用不著,而更多的是用DOMContentLoaded
來儘早的的操作dom
。
另外還有image
、iframe
以及外聯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
的載入執行過程如下:
第二種外聯defer javascript
稍有不同,html
解析過程中遇到此類<script>
標籤不阻塞解析,而是會暫存到一個佇列中,等整個html
解析完成後再按佇列的順序請求並執行javascript
,但是這種外聯defer javascript
全部載入並執行完成後才會派發DOMContentLoaded
事件,外聯defer javascript
的載入執行過程如下:
第三種外聯async javascript
則不阻塞html
的解析過程,注意這裡是說的指令碼的下載
過程不阻塞html
解析,如果下載完成後html
還沒解析完成,則會暫停html
解析,先執行完成下載後的javascript
程式碼再繼續解析html
,過程如下:
但是如果html
已經解析完畢,外聯async javascript
還未下載完成,則不阻塞DOMContentLoaded
事件的派發。因此外聯async javascript
很有可能來不及監聽DOMContentLoaded
事件,比如stackoverflow
上的這個問題。
說明下,這幾個圖引用自這裡。
3 DOMContentLoaded
相容性問題
DOMContentLoaded
最開始由firefox
提出,其他瀏覽器覺得非常有用也相繼開始支援,但是特性卻稍有不同,比如opera
中javascript
的執行並不等待外聯css
的載入。直到HTML5
出來後將DOMContentLoaded
標準化,依照HTML5
標準,javascript
指令碼執行前,出現在當前<script>
之前的<link rel="stylesheet">
必須完全載入。
那麼在所有瀏覽器標準化之前怎麼解決DOMContentLoaded
的相容性問題呢?可以參考jQuery
中.ready()
方法的實現,對於該方法的原始碼分析網上已經一大堆了,這裡就不做分析了,直接說下原理。其實是就是用了MDN: DOMContentLoaded中介紹的相容性方法,ie9
才開始支援DOMContentedLoaded
,ie8
環境可以通過檢測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.readystate
為interactive
或completed
的狀態,因為外聯async javascript
是不阻塞dom
解析的,因此為了完全覆蓋前面的4種情況,需要監聽document.readystate
的變化:
if (document.readystate === 'interactive'
|| document.readystate === 'complete') {
// 呼叫ready回撥函式
} else {
document.onreadystatechange = function () {
if (document.readystate === 'interative') {
// 呼叫ready回撥函式
}
}
}
複製程式碼
4 引用
主要參考了以下文章,推薦閱讀: