# Page Lifecycle API
Android、iOS 和最新的 Windows 系統可以隨時自主地停止后臺進程,及時釋放系統資源。也就是說,網頁可能隨時被系統丟棄掉。以前的瀏覽器 API 完全沒有考慮到這種情況,導致開發者根本沒有辦法監聽到系統丟棄頁面。
為了解決這個問題,W3C 新制定了一個 Page Lifecycle API,統一了網頁從誕生到卸載的行為模式,并且定義了新的事件,允許開發者響應網頁狀態的各種轉換。
有了這個 API,開發者就可以預測網頁下一步的狀態,從而進行各種針對性的處理。Chrome 68 支持這個 API,對于老式瀏覽器可以使用谷歌開發的兼容庫 [PageLifecycle.js](https://github.com/GoogleChromeLabs/page-lifecycle)。
## 生命周期階段
網頁的生命周期分成六個階段,每個時刻只可能處于其中一個階段。

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