Page Lifecycle API 教程

阮一峰發表於2018-11-05

兩週前,我介紹了 Page Visibility API。有了它,就可以監聽各種情況的網頁解除安裝。

但是,它沒有解決一個問題。Android、iOS 和最新的 Windows 系統可以隨時自主地停止後臺程式,及時釋放系統資源。也就是說,網頁可能隨時被系統丟棄掉。Page Visibility API 只在網頁對使用者不可見時觸發,至於網頁會不會被系統丟棄掉,它就無能為力了。

為了解決這個問題,W3C 新制定了一個 Page Lifecycle API,統一了網頁從誕生到解除安裝的行為模式,並且定義了新的事件,允許開發者響應網頁狀態的各種轉換。

有了這個 API,開發者就可以預測網頁下一步的狀態,從而進行各種針對性的處理。Chrome 68 支援這個 API,對於老式瀏覽器可以使用谷歌開發的相容庫 PageLifecycle.js

一、生命週期階段

網頁的生命週期分成六個階段,每個時刻只可能處於其中一個階段。

(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>網頁跟頂層網頁一樣,都會同時監聽到下面的事件。

3.1 focus 事件

focus事件在頁面獲得輸入焦點時觸發,比如網頁從 Passive 階段變為 Active 階段。

3.2 blur 事件

blur事件在頁面失去輸入焦點時觸發,比如網頁從 Active 階段變為 Passive 階段。

3.3 visibilitychange 事件

visibilitychange事件在網頁可見狀態發生變化時觸發,一般發生在以下幾種場景。

  • 使用者隱藏頁面(切換 Tab、最小化瀏覽器),頁面由 Active 階段變成 Hidden 階段。
  • 使用者重新訪問隱藏的頁面,頁面由 Hidden 階段變成 Active 階段。
  • 使用者關閉頁面,頁面會先進入 Hidden 階段,然後進入 Terminated 階段。

可以透過document.onvisibilitychange屬性指定這個事件的回撥函式。

3.4 freeze 事件

freeze事件在網頁進入 Frozen 階段時觸發。

可以透過document.onfreeze屬性指定在進入 Frozen 階段時呼叫的回撥函式。


function handleFreeze(e) {
  // Handle transition to FROZEN
}
document.addEventListener('freeze', handleFreeze);

# 或者
document.onfreeze = function() { ... }

這個事件的監聽函式,最長只能執行500毫秒。並且只能複用已經開啟的網路連線,不能發起新的網路請求。

注意,從 Frozen 階段進入 Discarded 階段,不會觸發任何事件,無法指定回撥函式,只能在進入 Frozen 階段時指定回撥函式。

3.5 resume 事件

resume事件在網頁離開 Frozen 階段,變為 Active / Passive / Hidden 階段時觸發。

document.onresume屬性指的是頁面離開 Frozen 階段、進入可用狀態時呼叫的回撥函式。


function handleResume(e) {
  // handle state transition FROZEN -> ACTIVE
}
document.addEventListener("resume", handleResume);

# 或者
document.onresume = function() { ... }

3.6 pageshow 事件

pageshow事件在使用者載入網頁時觸發。這時,有可能是全新的頁面載入,也可能是從快取中獲取的頁面。如果是從快取中獲取,則該事件物件的event.persisted屬性為true,否則為false

這個事件的名字有點誤導,它跟頁面的可見性其實毫無關係,只跟瀏覽器的 History 記錄的變化有關。

3.7 pagehide 事件

pagehide事件在使用者離開當前網頁、進入另一個網頁時觸發。它的前提是瀏覽器的 History 記錄必須發生變化,跟網頁是否可見無關。

如果瀏覽器能夠將當前頁面新增到快取以供稍後重用,則事件物件的event.persisted屬性為true。 如果為true。如果頁面新增到了快取,則頁面進入 Frozen 狀態,否則進入 Terminatied 狀態。

3.8 beforeunload 事件

beforeunload事件在視窗或文件即將解除安裝時觸發。該事件發生時,文件仍然可見,此時解除安裝仍可取消。經過這個事件,網頁進入 Terminated 狀態。

3.9 unload 事件

unload事件在頁面正在解除安裝時觸發。經過這個事件,網頁進入 Terminated 狀態。

四、獲取當前階段

如果網頁處於 Active、Passive 或 Hidden 階段,可以透過下面的程式碼,獲得網頁當前的狀態。


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屬性,瞭解先前的網頁是否被丟棄了。


if (document.wasDiscarded) {
  // 該網頁已經不是原來的狀態了,曾經被瀏覽器丟棄過
  // 恢復以前的狀態
  getPersistedState(self.discardedClientId);
}

同時,window物件上會新增window.clientIdwindow.discardedClientId兩個屬性,用來恢復丟棄前的狀態。

六、參考連結

(完)

相關文章