頁面生命周期:DOMContentLoaded, load, beforeunload, unload
HTML頁面的生命周期有以下三個(gè)重要事件:
-
DOMContentLoaded— 瀏覽器已經(jīng)完全加載了HTML,DOM樹已經(jīng)構(gòu)建完畢,但是像是<img>和樣式表等外部資源可能并沒有下載完畢。 -
load— 瀏覽器已經(jīng)加載了所有的資源(圖像,樣式表等)。 -
beforeunload/unload-- 當(dāng)用戶離開頁面的時(shí)候觸發(fā)。
每個(gè)事件都有特定的用途
-
DOMContentLoaded-- DOM加載完畢,所以js可以訪問所有DOM節(jié)點(diǎn),初始化界面。 -
load-- 附加資源已經(jīng)加載完畢,可以在此事件觸發(fā)時(shí)獲得圖像的大小(如果沒有被在HTML/CSS中指定) -
beforeunload/unload-- 用戶正在離開頁面:可以詢問用戶是否保存了更改以及是否確定要離開頁面。
來看一下每個(gè)事件的細(xì)節(jié)。
DOMContentLoaded
DOMContentLoaded 由 document 對(duì)象觸發(fā)。
我們使用 addEventListener 來監(jiān)聽它:
document.addEventListener("DOMContentLoaded", ready);
舉個(gè)例子
<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">
在這個(gè)例子中 DOMContentLoaded在document加載完成后就被觸發(fā),無需等待其他資源的載入,所以alert輸出的圖像的大小為0。
這么看來DOMContentLoaded 似乎很簡(jiǎn)單,DOM樹構(gòu)建完畢之后就運(yùn)行該事件,不過其實(shí)存在一些陷阱。
DOMContentLoaded 和腳本
當(dāng)瀏覽器在解析HTML頁面時(shí)遇到了 <script>...</script> 標(biāo)簽,將無法繼續(xù)構(gòu)建DOM樹(譯注:UI渲染線程與JS引擎是互斥的,當(dāng)JS引擎執(zhí)行時(shí)UI線程會(huì)被掛起),必須立即執(zhí)行腳本。所以 DOMContentLoaded 有可能在所有腳本執(zhí)行完畢后觸發(fā)。
外部腳本(帶src的)的加載和解析也會(huì)暫停DOM樹構(gòu)建,所以 DOMContentLoaded 也會(huì)等待外部腳本。
不過有兩個(gè)例外是帶async和defer的外部腳本,他們告訴瀏覽器繼續(xù)解析而不需要等待腳本的執(zhí)行,所以用戶可以在腳本加載完成前可以看到頁面,有較好的用戶體驗(yàn)。
async和defer屬性僅僅對(duì)外部腳本起作用,并且他們?cè)?code>src不存在時(shí)會(huì)被自動(dòng)忽略。
它們都告訴瀏覽器繼續(xù)處理頁面上的內(nèi)容,而在后臺(tái)加載腳本,然后在腳本加載完畢后再執(zhí)行。所以腳本不會(huì)阻塞DOM樹的構(gòu)建和頁面的渲染。
(譯注:其實(shí)這里是不對(duì)的,帶有async和defer的腳本的下載是和HTML的下載與解析是異步的,但是js的執(zhí)行一定是和UI線程是互斥的,像下面這張圖所示,async在下載完畢后的執(zhí)行會(huì)阻塞HTML的解析)

他們有兩處不同:
async |
defer |
|
|---|---|---|
| 順序 | 帶有async的腳本是優(yōu)先執(zhí)行先加載完的腳本,他們?cè)陧撁嬷械捻樞虿⒉挥绊懰麄儓?zhí)行的順序。 |
帶有defer的腳本按照他們?cè)陧撁嬷谐霈F(xiàn)的順序依次執(zhí)行。 |
DOMContentLoaded |
帶有async的腳本也許會(huì)在頁面沒有完全下載完之前就加載,這種情況會(huì)在腳本很小或本緩存,并且頁面很大的情況下發(fā)生。 |
帶有defer的腳本會(huì)在頁面加載和解析完畢后執(zhí)行,剛好在DOMContentLoaded之前執(zhí)行。 |
所以async用在那些完全不依賴其他腳本的腳本上。
### DOMContentLoaded and styles
External style sheets don't affect DOM, and so `DOMContentLoaded` does not wait for them.
外部樣式表并不會(huì)影響DOM,所以`DOMContentLoaded`并不會(huì)被他們阻塞。
But there's a pitfall: if we have a script after the style, then that script must wait for the stylesheet to execute:
不過仍然有一個(gè)陷阱:如果在樣式后面有一個(gè)內(nèi)聯(lián)腳本,那么腳本必須等待樣式先加載完。
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// the script doesn't not execute until the stylesheet is loaded
// 腳本直到樣式表加載完畢后才會(huì)執(zhí)行。
alert(getComputedStyle(document.body).marginTop);
</script>
發(fā)生這種事的原因是腳本也許會(huì)像上面的例子中所示,去得到一些元素的坐標(biāo)或者基于樣式的屬性。所以他們自然要等到樣式加載完畢才可以執(zhí)行。
DOMContentLoaded需要等待腳本的執(zhí)行,腳本又需要等待樣式的加載。
瀏覽器的自動(dòng)補(bǔ)全
Firefox, Chrome和Opera會(huì)在DOMContentLoaded執(zhí)行時(shí)自動(dòng)補(bǔ)全表單。
例如,如果頁面有登錄的界面,瀏覽器記住了該頁面的用戶名和密碼,那么在 DOMContentLoaded運(yùn)行的時(shí)候?yàn)g覽器會(huì)試圖自動(dòng)補(bǔ)全表單(如果用戶設(shè)置允許)。
所以如果DOMContentLoaded被一個(gè)需要長(zhǎng)時(shí)間執(zhí)行的腳本阻塞,那么自動(dòng)補(bǔ)全也會(huì)等待。你也許見過某些網(wǎng)站(如果你的瀏覽器開啟了自動(dòng)補(bǔ)全)—— 瀏覽器并不會(huì)立刻補(bǔ)全登錄項(xiàng),而是等到整個(gè)頁面加載完畢后才填充。這就是因?yàn)樵诘却?code>DOMContentLoaded事件。
使用帶async和defer的腳本的一個(gè)好處就是,他們不會(huì)阻塞DOMContentLoaded和瀏覽器自動(dòng)補(bǔ)全。(譯注:其實(shí)執(zhí)行還是會(huì)阻塞的)
window.onload
window對(duì)象上的onload事件在所有文件包括樣式表,圖片和其他資源下載完畢后觸發(fā)。
下面的例子正確檢測(cè)了圖片的大小,因?yàn)?code>window.onload會(huì)等待所有圖片的加載。
<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
用戶離開頁面的時(shí)候,window對(duì)象上的unload事件會(huì)被觸發(fā),我們可以做一些不存在延遲的事情,比如關(guān)閉彈出的窗口,可是我們無法阻止用戶轉(zhuǎn)移到另一個(gè)頁面上。
所以我們需要使用另一個(gè)事件 — onbeforeunload。
window.onbeforeunload
如果用戶即將離開頁面或者關(guān)閉窗口時(shí),beforeunload事件將會(huì)被觸發(fā)以進(jìn)行額外的確認(rèn)。
瀏覽器將顯示返回的字符串,舉個(gè)例子:
window.onbeforeunload = function() {
return "There are unsaved changes. Leave now?";
};
有些瀏覽器像Chrome和火狐會(huì)忽略返回的字符串取而代之顯示瀏覽器自身的文本,這是為了安全考慮,來保證用戶不受到錯(cuò)誤信息的誤導(dǎo)。
readyState
如果我們?cè)谡麄€(gè)頁面加載完畢后設(shè)置DOMContentLoaded會(huì)發(fā)生什么呢?
啥也沒有,DOMContentLoaded不會(huì)被觸發(fā)。
有一些情況我們無法確定頁面上是否已經(jīng)加載完畢,比如一個(gè)帶有async的外部腳本的加載和執(zhí)行是異步的(注:執(zhí)行并不是異步的-_-)。在不同的網(wǎng)絡(luò)狀況下,腳本有可能是在頁面加載完畢后執(zhí)行也有可能是在頁面加載完畢前執(zhí)行,我們無法確定。所以我們需要知道頁面加載的狀況。
document.readyState屬性給了我們加載的信息,有三個(gè)可能的值:
-
loading加載 - document仍在加載。 -
interactive互動(dòng) - 文檔已經(jīng)完成加載,文檔已被解析,但是諸如圖像,樣式表和框架之類的子資源仍在加載。 -
complete- 文檔和所有子資源已完成加載。狀態(tài)表示load事件即將被觸發(fā)。
所以我們可以檢查 document.readyState 的狀態(tài),如果沒有就緒可以選擇掛載事件,如果已經(jīng)就緒了就可以直接立即執(zhí)行。
像這樣:
function work() { /*...*/ }
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', work);
} else {
work();
}
每當(dāng)文檔的加載狀態(tài)改變的時(shí)候就有一個(gè)readystatechange事件被觸發(fā),所以我們可以打印所有的狀態(tài)。
// current state
console.log(document.readyState);
// print state changes
document.addEventListener('readystatechange', () => console.log(document.readyState));
readystatechange 是追蹤頁面加載的一個(gè)可選的方法,很早之前就已經(jīng)出現(xiàn)了。不過現(xiàn)在很少被使用了,為了保持完整性還是介紹一下它。
readystatechange的在各個(gè)事件中的執(zhí)行順序又是如何呢?
<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] initial readyState:loading
- [2] readyState:interactive
- [2] DOMContentLoaded
- [3] iframe onload
- [4] readyState:complete
- [4] img onload
- [4] window onload
方括號(hào)中的數(shù)字表示他們發(fā)生的時(shí)間,真實(shí)的發(fā)生時(shí)間會(huì)更晚一點(diǎn),不過相同數(shù)字的時(shí)間可以認(rèn)為是在同一時(shí)刻被按順序觸發(fā)(誤差在幾毫秒之內(nèi))
-
document.readyState在DOMContentLoaded前一刻變?yōu)?code>interactive,這兩個(gè)事件可以認(rèn)為是同時(shí)發(fā)生。 -
document.readyState在所有資源加載完畢后(包括iframe和img)變成complete,我們可以看到complete、img.onload和window.onload幾乎同時(shí)發(fā)生,區(qū)別就是window.onload在所有其他的load事件之后執(zhí)行。
總結(jié)
頁面事件的生命周期:
-
DOMContentLoaded事件在DOM樹構(gòu)建完畢后被觸發(fā),我們可以在這個(gè)階段使用js去訪問元素。-
async和defer的腳本可能還沒有執(zhí)行。 - 圖片及其他資源文件可能還在下載中。
-
load事件在頁面所有資源被加載完畢后觸發(fā),通常我們不會(huì)用到這個(gè)事件,因?yàn)槲覀儾恍枰饶敲淳谩?/p>beforeunload在用戶即將離開頁面時(shí)觸發(fā),它返回一個(gè)字符串,瀏覽器會(huì)向用戶展示并詢問這個(gè)字符串以確定是否離開。unload在用戶已經(jīng)離開時(shí)觸發(fā),我們?cè)谶@個(gè)階段僅可以做一些沒有延遲的操作,由于種種限制,很少被使用。-
document.readyState表征頁面的加載狀態(tài),可以在readystatechange中追蹤頁面的變化狀態(tài):-
loading— 頁面正在加載中。 -
interactive-- 頁面解析完畢,時(shí)間上和DOMContentLoaded同時(shí)發(fā)生,不過順序在它之前。 -
complete-- 頁面上的資源都已加載完畢,時(shí)間上和window.onload同時(shí)發(fā)生,不過順序在他之前。
-
原文地址:http://javascript.info/onload...
原文作者:fi3ework