重新理解:什么是页面生命周期API?

Page Lifecycle介绍

简而言之,在此之前,Web网页在浏览器内可以无限期地保持运行状态。随着大量网页的运行,内存、CPU、电池和网络等关键系统资源可能会被超额使用,然后就会出问题。

虽然 Web一直有和生命周期相关的事件(例如load、unload和visibilitychange),但这些事件只允许开发人员响应用户发起的生命周期状态变化。为了让Web页面能够更加稳定的运行,浏览器需要一种主动回收和重新分配系统资源的方法。

为了解决这个问题,W3C 新制定了一个 Page Lifecycle API,统一了网页从诞生到卸载的行为模式,并且定义了新的事件,允许开发者响应网页状态的各种转换。

有了这个 API,开发者就可以预测网页下一步的状态,从而进行各种针对性的处理。Chrome 68 开始支持这个 API,对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js

重新理解:什么是页面生命周期API?
生命周期
经验
之前在Chrome里面跑一个长时间循环的JS时,发现当电脑在一定时间内没有操作,JS运行就停止了。重新开始操作后,JS又会开始跑起来,现在想了就是生命周期的原因了。

生命周期API

1.Active 阶段

在 Active 阶段,页面处于可见状态,且拥有输入焦点。

之前

  • 通过focus事件,页面从Passive状态进入Active状态

之后

  • 通过blur事件,页面从Active状态进入Passive状态

2.Passive 阶段

在 Passive 阶段,网页可见,但没有输入焦点,无法接受输入。UI 更新(比如动画)仍然在执行。该阶段只可能发生在桌面同时有多个窗口的情况。

之前

  • 通过blur事件,页面从Active状态进入Passive状态。
  • 通过visibilitychange事件,从Hidden进入passive状态。

之后

  • 通过focus事件可进入Active状态。
  • 通过visibilitychange 事件,可进入Hidden状态。

3.Hidden 阶段

在 Hidden 阶段,用户的桌面被其他窗口占据,网页不可见,但尚未冻结。UI 更新不再执行。

之前

  • 通过visibilitychange事件,页面从passive状态进入Hidden 状态。

之后

  • 通过visibilitychange事件可进入passive状态。
  • 通过freeze事件,可进入frozen 状态。
  • 通过pagehide事件,可进入terminated状态。

4.Frozen 阶段

如果网页处于 Hidden 阶段的时间过久,用户又不关闭网页,浏览器就有可能冻结网页,使其进入 Frozen 阶段。不过,也有可能,处于可见状态的页面长时间没有操作,也会进入 Frozen 阶段。

这个阶段的特征是,网页不会再被分配 CPU 计算资源。定时器、回调函数、网络请求、DOM 操作都不会执行,不过正在运行的任务会执行完。浏览器可能会允许 Frozen 阶段的页面,周期性复苏一小段时间,短暂变回 Hidden 状态,允许一小部分任务执行。

之前

  • 通过freeze 事件,页面从Hidden 状态进入Frozen状态。

之后

  • 通过resume、pageshow事件,页面从Frozen状态进入active状态。
  • 通过resume、pageshow事件,页面从Frozen状态进入passive状态。
  • 通过resume事件,页面从Frozen状态进入Hidden 状态。

5.Terminated 阶段

在 Terminated 阶段,由于用户主动关闭窗口,或者在同一个窗口前往其他页面,导致当前页面开始被浏览器卸载并从内存中清除。注意,这个阶段总是在 Hidden 阶段之后发生,也就是说,用户主动离开当前页面,总是先进入 Hidden 阶段,再进入 Terminated 阶段。

这个阶段会导致网页卸载,任何新任务都不会在这个阶段启动,并且如果运行时间太长,正在进行的任务可能会被终止。

之前

  • 通过pagehide 事件,页面从hidden 状态进入Terminated 状态。

之后

6.Discarded 阶段

如果网页长时间处于 Frozen 阶段,用户又不唤醒页面,那么就会进入 Discarded 阶段,即浏览器自动卸载网页,清除该网页的内存占用。不过,Passive 阶段的网页如果长时间没有互动,也可能直接进入 Discarded 阶段。

这一般是在用户没有介入的情况下,由系统强制执行。任何类型的新任务或 JavaScript 代码,都不能在此阶段执行,因为这时通常处在资源限制的状况下。

网页被浏览器自动 Discarded 以后,它的 Tab 窗口还是在的。如果用户重新访问这个 Tab 页,浏览器将会重新向服务器发出请求,再一次重新加载网页,回到 Active 阶段。

提示
播放音频、使用 WebRTC、alert、Notification 时不会进入frozen和Terminated

相关事件

1.事件列表

  1. focus事件,DOM获得焦点
  2. blur事件,DOM失去焦点
  3. visibilitychange事件,visibilityState文档的visibilityState值已更改。当用户导航到新页面、切换选项卡、关闭选项卡、最小化或关闭浏览器或切换移动操作系统上的应用程序时,可能会发生这种情况。
  4. freeze事件,页面刚刚被冻结。页面任务队列中的任何可冻结任务都不会启动。
  5. resume事件,浏览器恢复冻结页面。
  6. pageshow事件,onpageshow 事件类似于 onload 事件,onload 事件在页面第一次加载时触发, onpageshow 事件在每次加载页面时触发,即 onload 事件在页面从浏览器缓存中读取时不触发。可以使用 PageTransitionEvent 对象的 persisted 属性来判断页面是否取自 Back-Forward Cache。事件的persisted属性为true代表取自往返缓存,否则为false。
  7. pagehide,页面隐藏事件。如果用户导航到另一个页面,并且浏览器将当前页面添加到Back-Forward Cache,则事件的persisted属性为true.,页面进入冻结状态,否则进入终止状态。
  8. beforeunload事件,页面关闭之前触发,该beforeunload事件应仅用于提醒用户未保存的更改。保存这些更改后,应删除该事件。
提示
load和DOMContentLoaded事件不表示生命周期状态更改,因此它们与生命周期 API 无关。

2.拓展

const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

addEventListener(
  terminationEvent,
  event => {
    // Note: if the browser is able to cache the page, `event.persisted`
    // is `true`, and the state is frozen rather than terminated.
  },
  {capture: true}
);

现代浏览器(包括 IE11)中,建议使用pagehide事件来检测页面卸载(也称为终止状态)而不是unload事件。

往返缓存(Back/Forward cache)
往返缓存(Back/Forward cache,下文中简称bfcache)是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略。该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到bfcache中;当用户点击后退按钮的时候,将页面直接从bfcache中加载,节省了网络请求的时间。
参考:https://segmentfault.com/a/1190000015321895

Document相关属性

  1. onfreeze,freeze事件监听
  2. onresume,resume事件监听
  3. wasDiscarded,页面是否被销毁
  4. visibilityState,页面的可见性
  5. hasFocus(),是否拥有焦点

生命周期事件测试

1.事件测试

const getState = () => {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};

// 获取初始状态
let state = getState();

// 状态改变时输出相关信息
const logStateChange = nextState => {
  const prevState = state;
  if (nextState !== prevState) {
    console.log(`State change: ${prevState} >>> ${nextState}`);
    state = nextState;
  }
};

// 监听相关生命周期事件
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach(type => {
  window.addEventListener(type, () => logStateChange(getState()), {
    capture: true,
  });
});

// 监听冻结事件
window.addEventListener(
  'freeze',
  () => {
    //frozen状态
    logStateChange('frozen');
  },
  {capture: true}
);

window.addEventListener(
  'pagehide',
  event => {
     // 判断下一个可能的事件
    if (event.persisted) {

      logStateChange('frozen');
    } else {
      logStateChange('terminated');
    }
  },
  {capture: true}
);
  • 并非所有页面生命周期事件都有相同的目标。pagehide、pageshow在window上触发;visibilitychange、freeze、resume在document上触发,focus 、 blur 在DOM自身触发。
  • 这些事件大多数不会冒泡,这意味着不可能将非捕获事件侦听器添加到一个共同的祖先元素并观察所有这些事件。
  • 事件捕获在事件冒泡之前执行,所以使用事件捕获能够保证事件被正常处理。
提示
上方代码在不同的浏览器中可能产生不同的结果,因为事件的执行顺序和实现并未完全统一。

2.浏览器差异

  • 一些浏览器在切换选项卡时不会触发Blur事件。这意味着页面可以从Active状态进入Hidden状态,而无需先经过passive状态。
  • 一些浏览器还没有实现freeze、resume事件,但是这种状态仍然可以通过pagehide、pageshow事件观察到。
  • 旧版本的 Internet Explorer(10 及以下)不支持visibilitychange事件。
  • pagehide和visibilitychange事件的调度顺序已经改变。在此之前,如果在卸载页面时页面的visibilitystate为true,visibilitychange会在pagehide 之后触发。现在visibilitychange必定在pagehide之前触发。
  • Safari在关闭选项卡时不会触发pagehide、visibilitychange 事件,因此在 Safari 中,还需要侦听beforeunload事件来检测hidden状态。但是由于可以取消beforeunload事件,因此您需要等到事件触发完成后才能知道状态是否已更改为hidden。这种方式只能在 Safari 中使用,因为在其他浏览器中使用此事件会损害性能。

beforeunload事件

建议仅在用户有未保存的更改时添加beforeunload侦听器,然后在保存未保存的更改后立即删除事件。

1.相关反例

addEventListener(
  'beforeunload',
  event => {
    // A function that returns `true` if the page has unsaved changes.
    if (pageHasUnsavedChanges()) {
      event.preventDefault();
      return (event.returnValue = 'Are you sure you want to exit?');
    }
  },
  {capture: true}
);

2.建议用法

const beforeUnloadListener = event => {
  event.preventDefault();
  return (event.returnValue = 'Are you sure you want to exit?');
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener, {capture: true});
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener, {capture: true});
});
提示
访问chrome://discards手动frozen、discard任意选项卡
网页不被挂起的情况
有活动的websocket连接、有正在播放的音频、正在使用WebUSB、Web Notifications API