你不知道的 DOMContentLoaded

寫在前面:本文是前端學習文章的其中一篇,查看全部文章可以關注我 Github 上的 front-end-study。

詳解 DOMContentLoaded

首先我們先直觀地感受下什麼是 DOMContentLoaded。打開 Chrome DevTools,切到 Network 面板,重新載入網頁,得到如下截圖:

標記 1 指向的藍線以及標記 2 指向的藍色字 「DOMContentLoaded:1.29s」 均表示 DOMContentLoaded 這個事件觸發的時間,只不過表現形式不同而已。

直觀地感受了 DOMContentLoaded,那它究竟是個什麼東東呢?

什麼是 DOMContentLoaded

有興趣的可以看下 W3C 的 HTML5 規範是如何描述 DOMContentLoaded 的,不感興趣也沒關係,我們繼續往下走。那麼我們可以看一下 MDN 對 DOMContentLoaded 的描述,啥,你還是不感興趣啊?也沒關係,接觸一個新概念時,沒人喜歡一上來就看那麼晦澀的文字的。那我下面就用通俗的語言跟你聊聊什麼是 DOMContentLoaded。

我們先來思考一個問題,如何衡量一個網頁的載入速度?

有人說可以用網頁載入完全的時間來衡量。我覺得這沒有問題,但不夠好。為什麼呢?在日常生活中,很多時候因為網路原因我們並不需要等待網頁上的所有內容都載入完畢,而是只需要載入主要內容就可以了,比如你打開這篇博客時,可能並不需要等所有圖片都載入完成,而是看到博客的正文就可以正常閱讀了。把上面的話提煉一下就是,用戶有時候只需要在空白的網頁上看見內容就可以了,而不需要等待所有內容都載入出來。那既然這樣,回到剛剛的問題,我覺得衡量一個網頁載入速度的一個方法就是「計算這個網頁從空白到出現內容所花費的時間」。那怎麼計算這段時間?HTML5 規範已經幫我們完成了相應的工作,就是當一個 HTML 文檔被載入和解析完成後,DOMContentLoaded 事件便會被觸發。

這時問題又來了,「HTML 文檔被載入和解析完成」是什麼意思呢?或者說,HTML 文檔被載入和解析完成之前,瀏覽器做了哪些事情呢?那我們需要從瀏覽器渲染原理來談談。

瀏覽器向伺服器請求到了 HTML 文檔後便開始解析,產物是 DOM(文檔對象模型),到這裡 HTML 文檔就被載入和解析完成了。如果有 CSS 的會根據 CSS 生成 CSSOM(CSS 對象模型),然後再由 DOM 和 CSSOM 合併產生渲染樹。有了渲染樹,知道了所有節點的樣式,下面便根據這些節點以及樣式計算它們在瀏覽器中確切的大小和位置,這就是布局階段。有了以上這些信息,下面就把節點繪製到瀏覽器上。所有的過程如下圖所示:

現在你可能了解 HTML 文檔被載入和解析完成前瀏覽器大概做了哪些工作,但還沒完,因為我們還沒有考慮現在前端的主角之一 JavaScript。

JavaScript 可以阻塞 DOM 的生成,也就是說當瀏覽器在解析 HTML 文檔時,如果遇到 <script>,便會停下對 HTML 文檔的解析,轉而去處理腳本。如果腳本是內聯的,瀏覽器會先去執行這段內聯的腳本,如果是外鏈的,那麼先會去載入腳本,然後執行。在處理完腳本之後,瀏覽器便繼續解析 HTML 文檔。看下面的例子:

<body>n <script type="text/javascript">n console.log(document.getElementById(ele)); // nulln </script>nn <div id="ele"></div>nn <script type="text/javascript">n console.log(document.getElementById(ele)); // <div id="ele"></div>n </script>n</body>n

另外,因為 JavaScript 可以查詢任意對象的樣式,所以意味著在 CSS 解析完成,也就是 CSSOM 生成之後,JavaScript 才可以被執行。

到這裡,我們可以總結一下。當文檔中沒有腳本時,瀏覽器解析完文檔便能觸發 DOMContentLoaded 事件;如果文檔中包含腳本,則腳本會阻塞文檔的解析,而腳本需要等 CSSOM 構建完成才能執行。在任何情況下,DOMContentLoaded 的觸發不需要等待圖片等其他資源載入完成。

非同步腳本

我們到這裡一直在說同步腳本對網頁渲染的影響,如果我們想讓頁面儘快顯示,那我們可以使用非同步腳本。HTML5 中定義了兩個定義非同步腳本的方法:defer 和 async。我們來看一看他們的區別。

同步腳本(標籤中不含 async 或 defer): <script src="***.js" charset="utf-8"></script>

當 HTML 文檔被解析時如果遇見(同步)腳本,則停止解析,先去載入腳本,然後執行,執行結束後繼續解析 HTML 文檔。過程如下圖:

defer 腳本:<script src="***.js" charset="utf-8" defer></script>

當 HTML 文檔被解析時如果遇見 defer 腳本,則在後台載入腳本,文檔解析過程不中斷,而等文檔解析結束之後,defer 腳本執行。另外,defer 腳本的執行順序與定義時的位置有關。過程如下圖:

async 腳本:<script src="***.js" charset="utf-8" async></script>

當 HTML 文檔被解析時如果遇見 async 腳本,則在後台載入腳本,文檔解析過程不中斷。腳本載入完成後,文檔停止解析,腳本執行,執行結束後文檔繼續解析。過程如下圖:

(圖片來源:async vs defer attributes)

如果你 Google "async 和 defer 的區別",你可能會發現一堆類似上面的文章或圖片,而在這裡,我想跟你分享的是 async 和 defer 對 DOMContentLoaded 事件觸發的影響。

defer 與 DOMContentLoaded

如果 script 標籤中包含 defer,那麼這一塊腳本將不會影響 HTML 文檔的解析,而是等到 HTML 解析完成後才會執行。而 DOMContentLoaded 只有在 defer 腳本執行結束後才會被觸發。 所以這意味著什麼呢?HTML 文檔解析不受影響,等 DOM 構建完成之後 defer 腳本執行,但腳本執行之前需要等待 CSSOM 構建完成。在 DOM、CSSOM 構建完畢,defer 腳本執行完成之後,DOMContentLoaded 事件觸發。

async 與 DOMContentLoaded

如果 script 標籤中包含 async,則 HTML 文檔構建不受影響,解析完畢後,DOMContentLoaded 觸發,而不需要等待 async 腳本執行、樣式表載入等等。

DOMContentLoaded 與 load

在回頭看第一張圖:

與標記 1 的藍線平行的還有一條紅線,紅線就代表 load 事件觸發的時間,對應的,在最下面的概覽部分,還有一個用紅色標記的 "Load:1.60s",描述了 load 事件觸發的具體時間。

這兩個事件有啥區別呢?點擊這個網頁你就能明白:https://testdrive-archive.azu...

解釋一下,當 HTML 文檔解析完成就會觸發 DOMContentLoaded,而所有資源載入完成之後,load 事件才會被觸發。

另外需要提一下的是,我們在 jQuery 中經常使用的 $(document).ready(function() { // ...代碼... }); 其實監聽的就是 DOMContentLoaded 事件,而 $(document).load(function() { // ...代碼... }); 監聽的是 load 事件。


推薦閱讀:

我的職業是前端工程師【二】: 入門不是應該很簡單嗎?
客戶要求幫他製作一個日曆,並實現查詢功能
【前端資訊】Webpack 4 將移除 CommonsChunkPlugin
工具武裝的前端開發工程師
「每日一題」談談你對原型、原型鏈、 Function、Object 的理解?

TAG:前端开发 | 前端工程师 | HTML5 |