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);
//?不是?"document.onDOMContentLoaded?=?..."
例如:
<script>function?ready()?{alert('DOM?is?ready');//?圖片目前尚未加載完成(除非已經被緩存),所以圖片的大小為?0x0alert(`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>
在上面這個例子中,我們首先會看到 "Library loaded...",然后才會看到 "DOM ready!"(所有腳本都已經執行結束)。
不會阻塞
DOMContentLoaded
的腳本:此規則有兩個例外:
具有
async
特性(attribute)的腳本不會阻塞DOMContentLoaded
,稍后[1] 我們會講到。使用
document.createElement('script')
動態生成并添加到網頁的腳本也不會阻塞DOMContentLoaded
。
DOMContentLoaded 和樣式
外部樣式表不會影響 DOM,因此 DOMContentLoaded
不會等待它們。
但這里有一個陷阱。如果在樣式后面有一個腳本,那么該腳本必須等待樣式表加載完成:
<link?type="text/css"?rel="stylesheet"?href="style.css">
<script>//?在樣式表加載完成之前,腳本都不會執行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()?{?//?與此相同?window.addEventListener('load',?(event)?=>?{alert('Page?loaded');//?此時圖片已經加載完成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
事件就會被觸發。我們可以在那里做一些不涉及延遲的操作,例如關閉相關的彈出窗口。
有一個值得注意的特殊情況是發送分析數據。
假設我們收集有關頁面使用情況的數據:鼠標點擊,滾動,被查看的頁面區域等。
自然地,當用戶要離開的時候,我們希望通過 unload
事件將數據保存到我們的服務器上。
有一個特殊的 navigator.sendBeacon(url, data)
方法可以滿足這種需求,詳見規范 https://w3c.github.io/beacon/。
它在后臺發送數據,轉換到另外一個頁面不會有延遲:瀏覽器離開頁面,但仍然在執行 sendBeacon
。
使用方式如下:
let?analyticsData?=?{?/*?帶有收集的數據的對象?*/?};window.addEventListener("unload",?function()?{navigator.sendBeacon("/analytics",?JSON.stringify(analyticsData));
});
請求以 POST 方式發送。
我們不僅能發送字符串,還能發送表單以及其他格式的數據,在 Fetch 一章有詳細講解,但通常它是一個字符串化的對象。
數據大小限制在 64kb。
當 sendBeacon
請求完成時,瀏覽器可能已經離開了文檔,所以就無法獲取服務器響應(對于分析數據來說通常為空)。
還有一個 keep-alive
標志,該標志用于在 fetch[2] 方法中為通用的網絡請求執行此類“離開頁面后”的請求。你可以在 Fetch API[3] 一章中找到更多相關信息。
如果我們要取消跳轉到另一頁面的操作,在這里做不到。但是我們可以使用另一個事件 —— onbeforeunload
。
window.onbeforeunload
如果訪問者觸發了離開頁面的導航(navigation)或試圖關閉窗口,beforeunload
處理程序將要求進行更多確認。
如果我們要取消事件,瀏覽器會詢問用戶是否確定。
你可以通過運行下面這段代碼,然后重新加載頁面來進行嘗試:
window.onbeforeunload?=?function()?{return?false;
};
由于歷史原因,返回非空字符串也被視為取消事件。在以前,瀏覽器曾經將其顯示為消息,但是根據 現代規范[4] 所述,它們不應該這樣。
這里有個例子:
window.onbeforeunload?=?function()?{return?"There?are?unsaved?changes.?Leave?now?";
};
它的行為已經改變了,因為有些站長通過顯示誤導性和惡意信息濫用了此事件處理程序。所以,目前一些舊的瀏覽器可能仍將其顯示為消息,但除此之外 —— 無法自定義顯示給用戶的消息。
readyState
如果我們將 DOMContentLoaded
事件處理程序設置在文檔加載完成之后,會發生什么?
很自然地,它永遠不會運行。
在某些情況下,我們不確定文檔是否已經準備就緒。我們希望我們的函數在 DOM 加載完成時執行,無論現在還是以后。
document.readyState
屬性可以為我們提供當前加載狀態的信息。
它有 3 個可能值:
loading
—— 文檔正在被加載。interactive
—— 文檔被全部讀取。complete
—— 文檔被全部讀取,并且所有資源(例如圖片等)都已加載完成。
所以,我們可以檢查 document.readyState
并設置一個處理程序,或在代碼準備就緒時立即執行它。
像這樣:
function?work()?{?/*...*/?}if?(document.readyState?==?'loading')?{//?仍在加載,等待事件document.addEventListener('DOMContentLoaded',?work);
}?else?{// DOM 已就緒!work();
}
還有一個 readystatechange
事件,會在狀態發生改變時觸發,因此我們可以打印所有這些狀態,就像這樣:
//?當前狀態
console.log(document.readyState);//?狀態改變時打印它
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="http://en.js.cx/clipart/train.gif"?id="img">
<script>img.onload?=?()?=>?log('img?onload');
</script>
此示例運行 在 sandbox 中[5]。
典型輸出:
[1] initial readyState:loading
[2] readyState:interactive
[2] DOMContentLoaded
[3] iframe onload
[4] img onload
[4] readyState:complete
[4] window onload
方括號中的數字表示發生這種情況的大致時間。標有相同數字的事件幾乎是同時發生的(+- 幾毫秒)。
在
DOMContentLoaded
之前,document.readyState
會立即變成interactive
。它們倆的意義實際上是相同的。當所有資源(
iframe
和img
)都加載完成后,document.readyState
變成complete
。這里我們可以發現,它與img.onload
(img
是最后一個資源)和window.onload
幾乎同時發生。轉換到complete
狀態的意義與window.onload
相同。區別在于window.onload
始終在所有其他load
處理程序之后運行。
總結
頁面生命周期事件:
當 DOM 準備就緒時,
document
上的DOMContentLoaded
事件就會被觸發。在這個階段,我們可以將 JavaScript 應用于元素。諸如
<script>...</script>
或<script src="..."></script>
之類的腳本會阻塞 ?DOMContentLoaded
,瀏覽器將等待它們執行結束。圖片和其他資源仍然可以繼續被加載。
當頁面和所有資源都加載完成時,
window
上的load
事件就會被觸發。我們很少使用它,因為通常無需等待那么長時間。當用戶想要離開頁面時,
window
上的beforeunload
事件就會被觸發。如果我們取消這個事件,瀏覽器就會詢問我們是否真的要離開(例如,我們有未保存的更改)。當用戶最終離開時,
window
上的unload
事件就會被觸發。在處理程序中,我們只能執行不涉及延遲或詢問用戶的簡單操作。正是由于這個限制,它很少被使用。我們可以使用navigator.sendBeacon
來發送網絡請求。document.readyState
是文檔的當前狀態,可以在readystatechange
事件中跟蹤狀態更改:loading
—— 文檔正在被加載。interactive
—— 文檔已被解析完成,與DOMContentLoaded
幾乎同時發生,但是在DOMContentLoaded
之前發生。complete
—— 文檔和資源均已加載完成,與window.onload
幾乎同時發生,但是在window.onload
之前發生。
現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 并列的 JavaScript 學習教程[6]。
在線免費閱讀:https://zh.javascript.info
參考資料
[1]
稍后: https://zh.javascript.info/script-async-defer
[2]fetch: https://zh.javascript.info/fetch
[3]Fetch API: https://zh.javascript.info/fetch-api
[4]現代規范: https://html.spec.whatwg.org/#unloading-documents
[5]在 sandbox 中: https://plnkr.co/edit/ct5SNvrHCA75b2KZ?p=preview
[6]React 官方文檔推薦,與 MDN 并列的 JavaScript 學習教程: https://zh-hans.reactjs.org/docs/getting-started.html#javascript-resources
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準備阿里P6/P7前端面試--項目經歷準備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
若川知乎高贊:有哪些必看的 JS庫?
末尾
你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學習源碼整體架構系列~(點擊藍字了解我)
關注
若川視野
,回復"pdf" 領取優質前端書籍pdf,回復"1",可加群長期交流學習我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯,可以點個
在看
呀^_^另外歡迎留言
交流~
精選前端好文,伴你不斷成長
若川原創文章精選!可點擊
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間
【源碼精選】
按鈕,歡迎點擊閱讀,也可以星標我的公眾號,便于查找