[譯]頁面生命週期:DOMContentLoaded, load, beforeunload, unload解析

fi3ework發表於2017-10-28

『 文章首發於GitHub Blog

原文地址:http://javascript.info/onload-ondomcontentloaded

HTML頁面的生命週期有以下三個重要事件:

  • DOMContentLoaded — 瀏覽器已經完全載入了HTML,DOM樹已經構建完畢,但是像是 <img> 和樣式表等外部資源可能並沒有下載完畢。
  • load — 瀏覽器已經載入了所有的資源(影象,樣式表等)。
  • beforeunload/unload -- 當使用者離開頁面的時候觸發。

每個事件都有特定的用途

  • DOMContentLoaded -- DOM載入完畢,所以js可以訪問所有DOM節點,初始化介面。
  • load -- 附加資源已經載入完畢,可以在此事件觸發時獲得影象的大小(如果沒有被在HTML/CSS中指定)
  • beforeunload/unload -- 使用者正在離開頁面:可以詢問使用者是否儲存了更改以及是否確定要離開頁面。

來看一下每個事件的細節。

DOMContentLoaded

DOMContentLoadeddocument 物件觸發。

我們使用 addEventListener 來監聽它:

document.addEventListener("DOMContentLoaded", ready);
複製程式碼

舉個例子

<script>
  function ready() {
    alert('DOM is ready');

    // image is not yet loaded (unless was cached), so the size is 0x0
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  }

  document.addEventListener("DOMContentLoaded", ready);
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
複製程式碼

在這個例子中 DOMContentLoaded在document載入完成後就被觸發,無需等待其他資源的載入,所以alert輸出的影象的大小為0。

這麼看來DOMContentLoaded 似乎很簡單,DOM樹構建完畢之後就執行該事件,不過其實存在一些陷阱。

DOMContentLoaded 和指令碼

當瀏覽器在解析HTML頁面時遇到了 <script>...</script> 標籤,將無法繼續構建DOM樹(譯註:UI渲染執行緒與JS引擎是互斥的,當JS引擎執行時UI執行緒會被掛起),必須立即執行指令碼。所以 DOMContentLoaded 有可能在所有指令碼執行完畢後觸發。

外部指令碼(帶src的)的載入和解析也會暫停DOM樹構建,所以 DOMContentLoaded 也會等待外部指令碼。

不過有兩個例外是帶asyncdefer的外部指令碼,他們告訴瀏覽器繼續解析而不需要等待指令碼的執行,所以使用者可以在指令碼載入完成前可以看到頁面,有較好的使用者體驗。

asyncdefer屬性僅僅對外部指令碼起作用,並且他們在src不存在時會被自動忽略。

它們都告訴瀏覽器繼續處理頁面上的內容,而在後臺載入指令碼,然後在指令碼載入完畢後再執行。所以指令碼不會阻塞DOM樹的構建和頁面的渲染。

(譯註:其實這裡是不對的,帶有asyncdefer的指令碼的下載是和HTML的下載與解析是非同步的,但是js的執行一定是和UI執行緒是互斥的,像下面這張圖所示,async在下載完畢後的執行會阻塞HTML的解析)

[譯]頁面生命週期:DOMContentLoaded, load, beforeunload, unload解析

他們有兩處不同:

async defer
順序 帶有async的指令碼是優先執行先載入完的指令碼,他們在頁面中的順序並不影響他們執行的順序。 帶有defer的指令碼按照他們在頁面中出現的順序依次執行。
DOMContentLoaded 帶有async的指令碼也許會在頁面沒有完全下載完之前就載入,這種情況會在指令碼很小或本快取,並且頁面很大的情況下發生。 帶有defer的指令碼會在頁面載入和解析完畢後執行,剛好在 DOMContentLoaded之前執行。

所以async用在那些完全不依賴其他指令碼的指令碼上。

### DOMContentLoaded and styles

External style sheets don't affect DOM, and so `DOMContentLoaded` does not wait for them.
外部樣式表並不會影響DOM,所以`DOMContentLoaded`並不會被他們阻塞。
But there's a pitfall: if we have a script after the style, then that script must wait for the stylesheet to execute:
不過仍然有一個陷阱:如果在樣式後面有一個內聯指令碼,那麼指令碼必須等待樣式先載入完。

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // the script doesn't not execute until the stylesheet is loaded
  // 指令碼直到樣式表載入完畢後才會執行。
  alert(getComputedStyle(document.body).marginTop);
</script>
複製程式碼

發生這種事的原因是指令碼也許會像上面的例子中所示,去得到一些元素的座標或者基於樣式的屬性。所以他們自然要等到樣式載入完畢才可以執行。

DOMContentLoaded需要等待指令碼的執行,指令碼又需要等待樣式的載入。

瀏覽器的自動補全

Firefox, Chrome和Opera會在DOMContentLoaded執行時自動補全表單。

例如,如果頁面有登入的介面,瀏覽器記住了該頁面的使用者名稱和密碼,那麼在 DOMContentLoaded執行的時候瀏覽器會試圖自動補全表單(如果使用者設定允許)。

所以如果DOMContentLoaded被一個需要長時間執行的指令碼阻塞,那麼自動補全也會等待。你也許見過某些網站(如果你的瀏覽器開啟了自動補全)—— 瀏覽器並不會立刻補全登入項,而是等到整個頁面載入完畢後才填充。這就是因為在等待DOMContentLoaded事件。

使用帶asyncdefer的指令碼的一個好處就是,他們不會阻塞DOMContentLoaded和瀏覽器自動補全。(譯註:其實執行還是會阻塞的)

2018.02.05:defer是會阻塞DOMContentLoaded的,被defer的指令碼要在DCL觸發前執行,所以如果HTML很快就載入完了(先不考慮CSS阻塞DLC的情況),而defer的指令碼還沒有載入完,瀏覽器就會等,等到指令碼載入完,執行完,再觸發DLC,放上一張圖(取自在devTool下分析自己寫的一個頁面)

image

可以看到,HTML很快就載入和解析完畢(CSS在這裡是動態載入的,不阻塞DLC),jquery和main.js的指令碼是defer的,DLC(藍線)一直在等,等到這兩個指令碼下載完並執行完,才觸發了DLC。 從這個角度看來,defer和把指令碼放在</body>前真是沒啥區別,只不過defer指令碼位於head中,更早被讀到,載入更早,而且不擔心會被其他的指令碼推遲下載開始的時間。

window.onload

window物件上的onload事件在所有檔案包括樣式表,圖片和其他資源下載完畢後觸發。

下面的例子正確檢測了圖片的大小,因為window.onload會等待所有圖片的載入。

<script>
  window.onload = function() {
    alert('Page loaded');

    // image is loaded at this time
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  };
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
複製程式碼

window.onunload

使用者離開頁面的時候,window物件上的unload事件會被觸發,我們可以做一些不存在延遲的事情,比如關閉彈出的視窗,可是我們無法阻止使用者轉移到另一個頁面上。

所以我們需要使用另一個事件 — onbeforeunload

window.onbeforeunload

如果使用者即將離開頁面或者關閉視窗時,beforeunload事件將會被觸發以進行額外的確認。

瀏覽器將顯示返回的字串,舉個例子:

window.onbeforeunload = function() {
  return "There are unsaved changes. Leave now?";
};
複製程式碼

有些瀏覽器像Chrome和火狐會忽略返回的字串取而代之顯示瀏覽器自身的文字,這是為了安全考慮,來保證使用者不受到錯誤資訊的誤導。

readyState

如果我們在整個頁面載入完畢後設定DOMContentLoaded會發生什麼呢?

啥也沒有,DOMContentLoaded不會被觸發。

有一些情況我們無法確定頁面上是否已經載入完畢,比如一個帶有async的外部指令碼的載入和執行是非同步的(注:執行並不是非同步的-_-)。在不同的網路狀況下,指令碼有可能是在頁面載入完畢後執行也有可能是在頁面載入完畢前執行,我們無法確定。所以我們需要知道頁面載入的狀況。

document.readyState屬性給了我們載入的資訊,有三個可能的值:

  • loading 載入 - document仍在載入。
  • interactive 互動 - 文件已經完成載入,文件已被解析,但是諸如影象,樣式表和框架之類的子資源仍在載入。
  • complete - 文件和所有子資源已完成載入。狀態表示 load 事件即將被觸發。

所以我們可以檢查 document.readyState 的狀態,如果沒有就緒可以選擇掛載事件,如果已經就緒了就可以直接立即執行。

像這樣:

function work() { /*...*/ }

if (document.readyState == 'loading') {
  document.addEventListener('DOMContentLoaded', work);
} else {
  work();
}
複製程式碼

每當文件的載入狀態改變的時候就有一個readystatechange事件被觸發,所以我們可以列印所有的狀態。

// current state
console.log(document.readyState);

// print state changes
document.addEventListener('readystatechange', () => console.log(document.readyState));
複製程式碼

readystatechange 是追蹤頁面載入的一個可選的方法,很早之前就已經出現了。不過現在很少被使用了,為了保持完整性還是介紹一下它。

readystatechange的在各個事件中的執行順序又是如何呢?

<script>
  function log(text) { /* output the time and message */ }
  log('initial readyState:' + document.readyState);

  document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
  document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));

  window.onload = () => log('window onload');
</script>

<iframe src="iframe.html" onload="log('iframe onload')"></iframe>

<img src="http://en.js.cx/clipart/train.gif" id="img">
<script>
  img.onload = () => log('img onload');
</script>
複製程式碼

輸出如下:

  1. [1] initial readyState:loading
  2. [2] readyState:interactive
  3. [2] DOMContentLoaded
  4. [3] iframe onload
  5. [4] readyState:complete
  6. [4] img onload
  7. [4] window onload

方括號中的數字表示他們發生的時間,真實的發生時間會更晚一點,不過相同數字的時間可以認為是在同一時刻被按順序觸發(誤差在幾毫秒之內)

  • document.readyStateDOMContentLoaded前一刻變為interactive,這兩個事件可以認為是同時發生。
  • document.readyState 在所有資源載入完畢後(包括iframeimg)變成complete,我們可以看到completeimg.onloadwindow.onload幾乎同時發生,區別就是window.onload在所有其他的load事件之後執行。

總結

頁面事件的生命週期:

  • DOMContentLoaded事件在DOM樹構建完畢後被觸發,我們可以在這個階段使用js去訪問元素。
    • asyncdefer的指令碼可能還沒有執行。
    • 圖片及其他資原始檔可能還在下載中。
  • load事件在頁面所有資源被載入完畢後觸發,通常我們不會用到這個事件,因為我們不需要等那麼久。
  • beforeunload在使用者即將離開頁面時觸發,它返回一個字串,瀏覽器會向使用者展示並詢問這個字串以確定是否離開。
  • unload在使用者已經離開時觸發,我們在這個階段僅可以做一些沒有延遲的操作,由於種種限制,很少被使用。
  • document.readyState表徵頁面的載入狀態,可以在readystatechange中追蹤頁面的變化狀態:
    • loading — 頁面正在載入中。
    • interactive -- 頁面解析完畢,時間上和 DOMContentLoaded同時發生,不過順序在它之前。
    • complete -- 頁面上的資源都已載入完畢,時間上和window.onload同時發生,不過順序在他之前。

相關文章