共计 5496 个字符,预计需要花费 14 分钟才能阅读完成。
Page Lifecycle 介绍
简而言之,在此之前,Web 网页在浏览器内可以无限期地保持运行状态。随着大量网页的运行,内存、CPU、电池和网络等关键系统资源可能会被超额使用,然后就会出问题。
虽然 Web 一直有和生命周期相关的事件(例如 load、unload 和 visibilitychange),但这些事件只允许开发人员响应用户发起的生命周期状态变化。为了让 Web 页面能够更加稳定的运行,浏览器需要一种主动回收和重新分配系统资源的方法。
为了解决这个问题,W3C 新制定了一个 Page Lifecycle API,统一了网页从诞生到卸载的行为模式,并且定义了新的事件,允许开发者响应网页状态的各种转换。
有了这个 API,开发者就可以预测网页下一步的状态,从而进行各种针对性的处理。Chrome 68 开始支持这个 API,对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js。
生命周期
经验
之前在 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. 事件列表
- focus 事件,DOM 获得焦点
- blur 事件,DOM 失去焦点
- visibilitychange 事件,visibilityState 文档的 visibilityState 值已更改。当用户导航到新页面、切换选项卡、关闭选项卡、最小化或关闭浏览器或切换移动操作系统上的应用程序时,可能会发生这种情况。
- freeze 事件,页面刚刚被冻结。页面任务队列中的任何可冻结任务都不会启动。
- resume 事件,浏览器恢复冻结页面。
- pageshow 事件,onpageshow 事件类似于 onload 事件,onload 事件在页面第一次加载时触发,onpageshow 事件在每次加载页面时触发,即 onload 事件在页面从浏览器缓存中读取时不触发。可以使用 PageTransitionEvent 对象的 persisted 属性来判断页面是否取自 Back-Forward Cache。事件的 persisted 属性为 true 代表取自往返缓存,否则为 false。
- pagehide,页面隐藏事件。如果用户导航到另一个页面,并且浏览器将当前页面添加到 Back-Forward Cache,则事件的 persisted 属性为 true.,页面进入冻结状态,否则进入终止状态。
- 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)
参考:https://segmentfault.com/a/1190000015321895
Document 相关属性
- onfreeze,freeze 事件监听
- onresume,resume 事件监听
- wasDiscarded,页面是否被销毁
- visibilityState,页面的可见性
- 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});
});
网页不被挂起的情况
有活动的 websocket 连接、有正在播放的音频、正在使用 WebUSB、Web Notifications API