深入瀏覽器原理系列(3)瀏覽器頁面也是有生命周期的
Android、iOS 和最新的 Windows 系統(tǒng)可以隨時(shí)自主地停止后臺(tái)進(jìn)程,及時(shí)釋放系統(tǒng)資源。也就是說,網(wǎng)頁可能隨時(shí)被系統(tǒng)丟棄掉。Page Visibility API 只在網(wǎng)頁對(duì)用戶不可見時(shí)觸發(fā),至于網(wǎng)頁會(huì)不會(huì)被系統(tǒng)丟棄掉,它就無能為力了。
為了解決這個(gè)問題,W3C 新制定了一個(gè) Page Lifecycle API,統(tǒng)一了網(wǎng)頁從誕生到卸載的行為模式,并且定義了新的事件,允許開發(fā)者響應(yīng)網(wǎng)頁狀態(tài)的各種轉(zhuǎn)換。
有了這個(gè) API,開發(fā)者就可以預(yù)測(cè)網(wǎng)頁下一步的狀態(tài),從而進(jìn)行各種針對(duì)性的處理。Chrome 68 支持這個(gè) API,對(duì)于老式瀏覽器可以使用谷歌開發(fā)的兼容庫 PageLifecycle.js。
一、生命周期階段
網(wǎng)頁的生命周期分成六個(gè)階段,每個(gè)時(shí)刻只可能處于其中一個(gè)階段。

(1)Active 階段
在 Active 階段,網(wǎng)頁處于可見狀態(tài),且擁有輸入焦點(diǎn)。
(2)Passive 階段
在 Passive 階段,網(wǎng)頁可見,但沒有輸入焦點(diǎn),無法接受輸入。UI 更新(比如動(dòng)畫)仍然在執(zhí)行。該階段只可能發(fā)生在桌面同時(shí)有多個(gè)窗口的情況。
(3)Hidden 階段
在 Hidden 階段,用戶的桌面被其他窗口占據(jù),網(wǎng)頁不可見,但尚未凍結(jié)。UI 更新不再執(zhí)行。
(4)Terminated 階段
在 Terminated 階段,由于用戶主動(dòng)關(guān)閉窗口,或者在同一個(gè)窗口前往其他頁面,導(dǎo)致當(dāng)前頁面開始被瀏覽器卸載并從內(nèi)存中清除。注意,這個(gè)階段總是在 Hidden 階段之后發(fā)生,也就是說,用戶主動(dòng)離開當(dāng)前頁面,總是先進(jìn)入 Hidden 階段,再進(jìn)入 Terminated 階段。
這個(gè)階段會(huì)導(dǎo)致網(wǎng)頁卸載,任何新任務(wù)都不會(huì)在這個(gè)階段啟動(dòng),并且如果運(yùn)行時(shí)間太長,正在進(jìn)行的任務(wù)可能會(huì)被終止。
(5)Frozen 階段
如果網(wǎng)頁處于 Hidden 階段的時(shí)間過久,用戶又不關(guān)閉網(wǎng)頁,瀏覽器就有可能凍結(jié)網(wǎng)頁,使其進(jìn)入 Frozen 階段。不過,也有可能,處于可見狀態(tài)的頁面長時(shí)間沒有操作,也會(huì)進(jìn)入 Frozen 階段。
這個(gè)階段的特征是,網(wǎng)頁不會(huì)再被分配 CPU 計(jì)算資源。定時(shí)器、回調(diào)函數(shù)、網(wǎng)絡(luò)請(qǐng)求、DOM 操作都不會(huì)執(zhí)行,不過正在運(yùn)行的任務(wù)會(huì)執(zhí)行完。瀏覽器可能會(huì)允許 Frozen 階段的頁面,周期性復(fù)蘇一小段時(shí)間,短暫變回 Hidden 狀態(tài),允許一小部分任務(wù)執(zhí)行。
(6)Discarded 階段
如果網(wǎng)頁長時(shí)間處于 Frozen 階段,用戶又不喚醒頁面,那么就會(huì)進(jìn)入 Discarded 階段,即瀏覽器自動(dòng)卸載網(wǎng)頁,清除該網(wǎng)頁的內(nèi)存占用。不過,Passive 階段的網(wǎng)頁如果長時(shí)間沒有互動(dòng),也可能直接進(jìn)入 Discarded 階段。
這一般是在用戶沒有介入的情況下,由系統(tǒng)強(qiáng)制執(zhí)行。任何類型的新任務(wù)或 JavaScript 代碼,都不能在此階段執(zhí)行,因?yàn)檫@時(shí)通常處在資源限制的狀況下。
網(wǎng)頁被瀏覽器自動(dòng) Discarded 以后,它的 Tab 窗口還是在的。如果用戶重新訪問這個(gè) Tab 頁,瀏覽器將會(huì)重新向服務(wù)器發(fā)出請(qǐng)求,再一次重新加載網(wǎng)頁,回到 Active 階段。
二、常見場景
以下是幾個(gè)常見場景的網(wǎng)頁生命周期變化。
(1)用戶打開網(wǎng)頁后,又切換到其他 App,但只過了一會(huì)又回到網(wǎng)頁。
網(wǎng)頁由 Active 變成 Hidden,又變回 Active。
(2)用戶打開網(wǎng)頁后,又切換到其他 App,并且長時(shí)候使用后者,導(dǎo)致系統(tǒng)自動(dòng)丟棄網(wǎng)頁。
網(wǎng)頁由 Active 變成 Hidden,再變成 Frozen,最后 Discarded。
(3)用戶打開網(wǎng)頁后,又切換到其他 App,然后從任務(wù)管理器里面將瀏覽器進(jìn)程清除。
網(wǎng)頁由 Active 變成 Hidden,然后 Terminated。
(4)系統(tǒng)丟棄了某個(gè) Tab 里面的頁面后,用戶重新打開這個(gè) Tab。
網(wǎng)頁由 Discarded 變成 Active。
三、事件
生命周期的各個(gè)階段都有自己的事件,以供開發(fā)者指定監(jiān)聽函數(shù)。這些事件里面,只有兩個(gè)是新定義的(freeze事件和resume事件),其它都是現(xiàn)有的。
注意,網(wǎng)頁的生命周期事件是在所有幀(frame)觸發(fā),不管是底層的幀,還是內(nèi)嵌的幀。也就是說,內(nèi)嵌的<iframe>網(wǎng)頁跟頂層網(wǎng)頁一樣,都會(huì)同時(shí)監(jiān)聽到下面的事件。
3.1 focus 事件
focus事件在頁面獲得輸入焦點(diǎn)時(shí)觸發(fā),比如網(wǎng)頁從 Passive 階段變?yōu)?Active 階段。
3.2 blur 事件
blur事件在頁面失去輸入焦點(diǎn)時(shí)觸發(fā),比如網(wǎng)頁從 Active 階段變?yōu)?Passive 階段。
3.3 visibilitychange 事件
visibilitychange事件在網(wǎng)頁可見狀態(tài)發(fā)生變化時(shí)觸發(fā),一般發(fā)生在以下幾種場景。
用戶隱藏頁面(切換 Tab、最小化瀏覽器),頁面由 Active 階段變成 Hidden 階段。
用戶重新訪問隱藏的頁面,頁面由 Hidden 階段變成 Active 階段。
用戶關(guān)閉頁面,頁面會(huì)先進(jìn)入 Hidden 階段,然后進(jìn)入 Terminated 階段。
可以通過document.onvisibilitychange屬性指定這個(gè)事件的回調(diào)函數(shù)。
3.4 freeze 事件
freeze事件在網(wǎng)頁進(jìn)入 Frozen 階段時(shí)觸發(fā)。
可以通過document.onfreeze屬性指定在進(jìn)入 Frozen 階段時(shí)調(diào)用的回調(diào)函數(shù)。
function handleFreeze(e) {
// Handle transition to FROZEN
}
document.addEventListener('freeze', handleFreeze);
# 或者
document.onfreeze = function() { ... }這個(gè)事件的監(jiān)聽函數(shù),最長只能運(yùn)行500毫秒。并且只能復(fù)用已經(jīng)打開的網(wǎng)絡(luò)連接,不能發(fā)起新的網(wǎng)絡(luò)請(qǐng)求。
注意,從 Frozen 階段進(jìn)入 Discarded 階段,不會(huì)觸發(fā)任何事件,無法指定回調(diào)函數(shù),只能在進(jìn)入 Frozen 階段時(shí)指定回調(diào)函數(shù)。
3.5 resume 事件
resume事件在網(wǎng)頁離開 Frozen 階段,變?yōu)?Active / Passive / Hidden 階段時(shí)觸發(fā)。
document.onresume屬性指的是頁面離開 Frozen 階段、進(jìn)入可用狀態(tài)時(shí)調(diào)用的回調(diào)函數(shù)。
function handleResume(e) {
// handle state transition FROZEN -> ACTIVE
}
document.addEventListener("resume", handleResume);
# 或者
document.onresume = function() { ... }3.6 pageshow 事件
pageshow事件在用戶加載網(wǎng)頁時(shí)觸發(fā)。這時(shí),有可能是全新的頁面加載,也可能是從緩存中獲取的頁面。如果是從緩存中獲取,則該事件對(duì)象的event.persisted屬性為true,否則為false。
這個(gè)事件的名字有點(diǎn)誤導(dǎo),它跟頁面的可見性其實(shí)毫無關(guān)系,只跟瀏覽器的 History 記錄的變化有關(guān)。
3.7 pagehide 事件
pagehide事件在用戶離開當(dāng)前網(wǎng)頁、進(jìn)入另一個(gè)網(wǎng)頁時(shí)觸發(fā)。它的前提是瀏覽器的 History 記錄必須發(fā)生變化,跟網(wǎng)頁是否可見無關(guān)。
如果瀏覽器能夠?qū)?dāng)前頁面添加到緩存以供稍后重用,則事件對(duì)象的event.persisted屬性為true。如果為true。如果頁面添加到了緩存,則頁面進(jìn)入 Frozen 狀態(tài),否則進(jìn)入 Terminatied 狀態(tài)。
3.8 beforeunload 事件
beforeunload事件在窗口或文檔即將卸載時(shí)觸發(fā)。該事件發(fā)生時(shí),文檔仍然可見,此時(shí)卸載仍可取消。經(jīng)過這個(gè)事件,網(wǎng)頁進(jìn)入 Terminated 狀態(tài)。
3.9 unload 事件
unload事件在頁面正在卸載時(shí)觸發(fā)。經(jīng)過這個(gè)事件,網(wǎng)頁進(jìn)入 Terminated 狀態(tài)。
四、獲取當(dāng)前階段
如果網(wǎng)頁處于 Active、Passive 或 Hidden 階段,可以通過下面的代碼,獲得網(wǎng)頁當(dāng)前的狀態(tài)。
const getState = () => {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
};如果網(wǎng)頁處于 Frozen 和 Terminated 狀態(tài),由于定時(shí)器代碼不會(huì)執(zhí)行,只能通過事件監(jiān)聽判斷狀態(tài)。進(jìn)入 Frozen 階段,可以監(jiān)聽freeze事件;進(jìn)入 Terminated 階段,可以監(jiān)聽pagehide事件。
五、document.wasDiscarded
如果某個(gè)選項(xiàng)卡處于 Frozen 階段,就隨時(shí)有可能被系統(tǒng)丟棄,進(jìn)入 Discarded 階段。如果后來用戶再次點(diǎn)擊該選項(xiàng)卡,瀏覽器會(huì)重新加載該頁面。
這時(shí),開發(fā)者可以通過判斷document.wasDiscarded屬性,了解先前的網(wǎng)頁是否被丟棄了。
if (document.wasDiscarded) {
// 該網(wǎng)頁已經(jīng)不是原來的狀態(tài)了,曾經(jīng)被瀏覽器丟棄過
// 恢復(fù)以前的狀態(tài)
getPersistedState(self.discardedClientId);
}同時(shí),window對(duì)象上會(huì)新增window.clientId和window.discardedClientId兩個(gè)屬性,用來恢復(fù)丟棄前的狀態(tài)。
六、參考鏈接
Page Lifecycle API, Philip Walton
Lifecycle API for Web Pages, W3C
Page Lifecycle 1 Editor’s Draft, W3C
