Page Lifecycle API 教程

兩週前,我介紹了 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兩個屬性,用來恢復丟棄前的狀態。

六、參考鏈接

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章