2022 年 10 月 14 日

頁面:DOMContentLoaded、load、beforeunload、unload

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

  • DOMContentLoaded – 瀏覽器已完全載入 HTML,並建構好 DOM 樹,但外部資源(例如圖片 <img> 和樣式表)可能尚未載入。
  • load – 不僅載入 HTML,也載入所有外部資源:圖片、樣式等。
  • beforeunload/unload – 使用者離開頁面。

每個事件都可能很有用

  • DOMContentLoaded 事件 – DOM 已準備好,因此處理常式可以查詢 DOM 節點,初始化介面。
  • load 事件 – 外部資源已載入,因此樣式已套用、已知影像大小等。
  • beforeunload 事件 – 使用者即將離開:我們可以檢查使用者是否已儲存變更,並詢問他們是否真的要離開。
  • unload – 使用者幾乎已離開,但我們仍然可以啟動一些作業,例如傳送統計資料。

讓我們探討這些事件的詳細資料。

DOMContentLoaded

DOMContentLoaded 事件發生在 document 物件上。

我們必須使用 addEventListener 來捕捉它

document.addEventListener("DOMContentLoaded", ready);
// not "document.onDOMContentLoaded = ..."

例如

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

    // image is not yet loaded (unless it 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 處理常式在文件載入時執行,因此它可以看到所有元素,包括下方的 <img>

但它不會等待影像載入。因此 alert 會顯示零大小。

乍看之下,DOMContentLoaded 事件非常簡單。DOM 樹已準備好 – 以下是事件。不過有一些特殊之處。

DOMContentLoaded 和腳本

當瀏覽器處理 HTML 文件並遇到 <script> 標籤時,它需要在繼續建置 DOM 之前執行。這是一個預防措施,因為腳本可能想要修改 DOM,甚至 document.write 到其中,因此 DOMContentLoaded 必須等待。

因此 DOMContentLoaded 肯定會在這些腳本之後發生

<script>
  document.addEventListener("DOMContentLoaded", () => {
    alert("DOM ready!");
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>

<script>
  alert("Library loaded, inline script executed");
</script>

在上面的範例中,我們首先看到「程式庫已載入…」,然後看到「DOM 已準備好!」(所有腳本都已執行)。

不會封鎖 DOMContentLoaded 的腳本

此規則有兩個例外

  1. 具有 async 屬性的腳本,我們將在 稍後介紹,不會封鎖 DOMContentLoaded
  2. 使用 document.createElement('script') 動態產生的腳本,然後新增到網頁中,也不會封鎖此事件。

DOMContentLoaded 和樣式

外部樣式表不會影響 DOM,因此 DOMContentLoaded 不會等待它們。

但有一個陷阱。如果我們在樣式之後有一個腳本,那麼該腳本必須等到樣式表載入

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // the script doesn't execute until the stylesheet is loaded
  alert(getComputedStyle(document.body).marginTop);
</script>

這是因為腳本可能想要取得元素的座標和其他依樣式而定的屬性,就像上面的範例一樣。自然而然地,它必須等到樣式載入。

由於 DOMContentLoaded 會等待腳本,現在它也會在腳本之前等待樣式。

內建瀏覽器自動填寫

Firefox、Chrome 和 Opera 會在 DOMContentLoaded 上自動填寫表單。

例如,如果頁面有一個包含登入和密碼的表單,而且瀏覽器記住了這些值,那麼在 DOMContentLoaded 上,它可能會嘗試自動填寫這些值(如果使用者同意)。

因此,如果 DOMContentLoaded 被載入時間長的腳本延後,那麼自動填寫也會等待。你可能在某些網站上看過(如果你使用瀏覽器自動填寫)——登入/密碼欄位並不會立即自動填寫,而是會延遲到頁面完全載入。那實際上就是延遲到 DOMContentLoaded 事件。

window.onload

window 物件上的 load 事件會在整個頁面載入,包括樣式、圖片和其他資源時觸發。此事件可透過 onload 屬性取得。

下面的範例正確顯示圖片大小,因為 window.onload 會等待所有圖片

<script>
  window.onload = function() { // can also use window.addEventListener('load', (event) => {
    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

當訪客離開頁面時,unload 事件會在 window 上觸發。我們可以在那裡做一些不涉及延遲的事情,例如關閉相關的彈出式視窗。

值得注意的例外是傳送分析資料。

假設我們收集有關頁面如何使用的資料:滑鼠點擊、捲動、檢視的頁面區域等等。

自然而然地,unload 事件是使用者離開我們時,我們會希望將資料儲存在我們的伺服器上。

規範 https://w3c.github.io/beacon/ 中描述了一種特殊的 navigator.sendBeacon(url, data) 方法,可用於此類需求。

它會在背景中傳送資料。不會延遲轉移到另一個頁面:瀏覽器會離開頁面,但仍會執行 sendBeacon

以下是使用方法

let analyticsData = { /* object with gathered data */ };

window.addEventListener("unload", function() {
  navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
  • 請求會以 POST 方式傳送。
  • 我們不僅可以傳送字串,還可以傳送表單和其他格式,如 Fetch 章節中所述,但通常它是一個字串化的物件。
  • 資料限制為 64kb。

sendBeacon 請求完成時,瀏覽器可能已經離開文件,因此無法取得伺服器回應(對於分析資料來說通常是空的)。

對於一般網路請求,fetch 方法中也有 keepalive 旗標,可用於執行此類「離開頁面後」請求。你可以在 Fetch API 章節中找到更多資訊。

如果我們想要取消轉移到另一個頁面,我們無法在此處執行此操作。但我們可以使用另一個事件——onbeforeunload

window.onbeforeunload

如果訪客離開頁面或嘗試關閉視窗,beforeunload 處理常式會要求額外的確認。

如果我們取消事件,瀏覽器可能會詢問訪客是否確定。

你可以執行這段程式碼並重新整理頁面來嘗試看看

window.onbeforeunload = function() {
  return false;
};

出於歷史原因,傳回非空字串也會視為取消事件。一段時間以前,瀏覽器會將其顯示為訊息,但正如 現代規格 所述,瀏覽器不應該這麼做。

以下是一個範例

window.onbeforeunload = function() {
  return "There are unsaved changes. Leave now?";
};

行為已變更,因為某些網站管理員會濫用此事件處理常式,顯示誤導性和令人討厭的訊息。因此,現在舊版瀏覽器仍然可能會將其顯示為訊息,但除此之外,沒有辦法自訂顯示給使用者的訊息。

event.preventDefault() 不會從 beforeunload 處理常式運作

這聽起來可能很奇怪,但大多數瀏覽器會忽略 event.preventDefault()

這表示下列程式碼可能無法運作

window.addEventListener("beforeunload", (event) => {
  // doesn't work, so this event handler doesn't do anything
  event.preventDefault();
});

相反地,在這些處理常式中,應該將 event.returnValue 設定為字串,以取得與上述程式碼類似的結果

window.addEventListener("beforeunload", (event) => {
  // works, same as returning from window.onbeforeunload
  event.returnValue = "There are unsaved changes. Leave now?";
});

readyState

如果我們在載入文件後設定 DOMContentLoaded 處理常式,會發生什麼事?

當然,它永遠不會執行。

有時候我們不確定文件是否已準備好。我們希望我們的函式在 DOM 載入時執行,無論是在現在或稍後。

document.readyState 屬性會告訴我們目前的載入狀態。

有 3 個可能的值

  • "loading" – 文件正在載入。
  • "interactive" – 文件已完全讀取。
  • "complete" – 文件已完全讀取,所有資源(例如圖片)也已載入。

因此,我們可以檢查 document.readyState,並在準備好時設定處理常式或執行程式碼。

像這樣

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

if (document.readyState == 'loading') {
  // still loading, wait for the event
  document.addEventListener('DOMContentLoaded', work);
} else {
  // DOM is ready!
  work();
}

還有 readystatechange 事件,當狀態變更時會觸發,因此我們可以像這樣列印所有這些狀態

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

// print state changes
document.addEventListener('readystatechange', () => console.log(document.readyState));

readystatechange 事件是追蹤文件載入狀態的另一種機制,它很早以前就出現了。現在很少使用它了。

讓我們看看完整的事件流程,以求完整性。

這裡有一個包含 <iframe><img> 和記錄事件處理程序的文件

<script>
  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="https://en.js.cx/clipart/train.gif" id="img">
<script>
  img.onload = () => log('img onload');
</script>

實際範例在 沙盒中

典型輸出

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

方括號中的數字表示發生時間的近似值。標記有相同數字的事件近似在同一時間發生 (± 幾毫秒)。

  • document.readyStateDOMContentLoaded 之前立即變為 interactive。這兩件事實際上是同一個意思。
  • 當所有資源(iframeimg)載入時,document.readyState 會變為 complete。這裡我們可以看到它發生在與 img.onloadimg 是最後一個資源)和 window.onload 大致相同的時間。切換到 complete 狀態與 window.onload 的意思相同。不同的是,window.onload 總是在所有其他 load 處理程序之後才執行。

摘要

頁面載入事件

  • 當 DOM 準備就緒時,DOMContentLoaded 事件會在 document 上觸發。我們可以在這個階段將 JavaScript 套用至元素。
    • 例如 <script>...</script><script src="..."></script> 的腳本會阻擋 DOMContentLoaded,瀏覽器會等待它們執行。
    • 影像和其他資源也可能繼續載入。
  • 當頁面和所有資源載入時,window 上的 load 事件會觸發。我們很少使用它,因為通常不需要等待這麼久。
  • 當使用者想要離開頁面時,window 上的 beforeunload 事件會觸發。如果我們取消事件,瀏覽器會詢問使用者是否真的想要離開(例如我們有未儲存的變更)。
  • 當使用者最終離開時,window 上的 unload 事件會觸發,在處理程序中我們只能執行不涉及延遲或詢問使用者的簡單操作。由於這個限制,它很少被使用。我們可以使用 navigator.sendBeacon 發送網路請求。
  • document.readyState 是文件的當前狀態,可以在 readystatechange 事件中追蹤變更
    • loading – 文件正在載入。
    • interactive – 文件已剖析,發生在與 DOMContentLoaded 大致相同但早於它的時間。
    • complete – 文件和資源已載入,發生在與 window.onload 大約相同時間,但發生在它之前。
教學地圖

留言

留言前請先閱讀…
  • 如果您有改善建議,請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入少數幾個字的程式碼,請使用 <code> 標籤,對於多行,請將它們包覆在 <pre> 標籤中,對於超過 10 行,請使用沙盒 (plnkrjsbincodepen…)