跳到主要内容

页面生命周期

问题

浏览器页面的生命周期有哪些阶段?各个事件的触发时机是什么?

答案

页面生命周期描述了网页从开始加载到完全卸载的整个过程。理解这些事件对于性能优化资源管理用户体验至关重要。

生命周期事件概览

加载阶段事件

DOMContentLoaded

触发时机:HTML 文档完全解析完成,DOM 树构建完成(不等待样式表、图片、iframe)。

document.addEventListener('DOMContentLoaded', (event: Event) => {
console.log('DOM 解析完成');
console.log('时间:', performance.now(), 'ms');

// 此时可以安全地操作 DOM
const app = document.getElementById('app');
console.log('app 元素:', app);
});

// 如果脚本在 DOMContentLoaded 之后执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

load

触发时机:所有资源(图片、样式表、iframe)都已加载完成。

window.addEventListener('load', (event: Event) => {
console.log('所有资源加载完成');

// 获取加载性能数据
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
console.log('DOM 加载时间:', timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart);
console.log('页面完全加载:', timing.loadEventEnd - timing.startTime);
});

readystatechange

触发时机document.readyState 变化时触发。

document.addEventListener('readystatechange', () => {
console.log('readyState:', document.readyState);
});

// readyState 的三个值:
// - 'loading': 正在加载
// - 'interactive': DOM 解析完成,等同于 DOMContentLoaded
// - 'complete': 所有资源加载完成,等同于 load

事件触发顺序

const log = (msg: string) => console.log(`${msg}: ${performance.now().toFixed(2)}ms`);

document.addEventListener('readystatechange', () => {
log(`readyState: ${document.readyState}`);
});

document.addEventListener('DOMContentLoaded', () => {
log('DOMContentLoaded');
});

window.addEventListener('load', () => {
log('load');
});

// 输出顺序:
// readyState: interactive
// DOMContentLoaded
// readyState: complete
// load

运行阶段事件

visibilitychange

触发时机:页面可见性变化时(切换标签页、最小化窗口等)。

document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('页面可见');
resumeVideo();
startAnimations();
} else {
console.log('页面隐藏');
pauseVideo();
stopAnimations();
}
});

// visibilityState 的值:
// - 'visible': 页面可见
// - 'hidden': 页面不可见

实际应用

// 1. 暂停/恢复视频
const video = document.querySelector('video') as HTMLVideoElement;

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
video.pause();
} else {
video.play();
}
});

// 2. 暂停/恢复轮询
class Poller {
private intervalId: number | null = null;
private interval: number;
private callback: () => void;

constructor(callback: () => void, interval: number) {
this.callback = callback;
this.interval = interval;

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.stop();
} else {
this.start();
}
});
}

start(): void {
if (this.intervalId === null) {
this.callback();
this.intervalId = window.setInterval(this.callback, this.interval);
}
}

stop(): void {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}

// 3. 统计页面停留时间
let startTime = Date.now();
let totalTime = 0;

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
totalTime += Date.now() - startTime;
console.log('累计停留时间:', totalTime, 'ms');
} else {
startTime = Date.now();
}
});

focus / blur

触发时机:窗口获得/失去焦点。

window.addEventListener('focus', () => {
console.log('窗口获得焦点');
});

window.addEventListener('blur', () => {
console.log('窗口失去焦点');
});

// 与 visibilitychange 的区别:
// - focus/blur: 窗口焦点变化
// - visibilitychange: 页面可见性变化
//
// 例如:在同一个窗口中切换 iframe 的焦点,
// 会触发 focus/blur,但不会触发 visibilitychange

卸载阶段事件

beforeunload

触发时机:页面即将卸载(关闭标签页、刷新、导航到其他页面)。

window.addEventListener('beforeunload', (event: BeforeUnloadEvent) => {
// 检查是否有未保存的更改
if (hasUnsavedChanges()) {
// 现代浏览器会显示标准提示,忽略自定义消息
event.preventDefault();
event.returnValue = ''; // 兼容旧浏览器
return '';
}
});

// 注意:不要滥用此事件,会影响用户体验
// 只在确实有未保存数据时使用

pagehide

触发时机:页面隐藏时(导航离开或进入 bfcache)。

window.addEventListener('pagehide', (event: PageTransitionEvent) => {
if (event.persisted) {
// 页面进入 bfcache,可能会被恢复
console.log('页面进入缓存');
} else {
// 页面真正卸载
console.log('页面卸载');
}
});

unload(不推荐)

触发时机:页面完全卸载。

// ⚠️ 不推荐使用 unload
// 1. 会阻止 bfcache
// 2. 不可靠,可能不会触发

window.addEventListener('unload', () => {
// 发送统计数据(使用 sendBeacon)
navigator.sendBeacon('/analytics', JSON.stringify({
event: 'page_unload',
time: Date.now(),
}));
});

pageshow

触发时机:页面显示时(首次加载或从 bfcache 恢复)。

window.addEventListener('pageshow', (event: PageTransitionEvent) => {
if (event.persisted) {
// 从 bfcache 恢复
console.log('从缓存恢复页面');
refreshData(); // 可能需要刷新数据
} else {
// 首次加载
console.log('首次加载页面');
}
});

Page Lifecycle API(现代标准)

Page Lifecycle API 提供了更细粒度的页面状态管理:

状态说明

状态描述可执行 JS
Active页面可见且有焦点
Passive页面可见但无焦点
Hidden页面不可见✅(限制)
Frozen页面被冻结,保存资源
Terminated页面正在卸载
Discarded页面被系统丢弃

获取页面状态

function getPageState(): string {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
}

// 监听状态变化
const events = ['focus', 'blur', 'visibilitychange', 'freeze', 'resume'];

events.forEach((event) => {
window.addEventListener(event, () => {
console.log('页面状态:', getPageState());
});
});

freeze / resume 事件

// 页面被冻结(Chrome)
document.addEventListener('freeze', () => {
console.log('页面被冻结');
// 保存状态到 sessionStorage
saveState();
});

// 页面恢复
document.addEventListener('resume', () => {
console.log('页面恢复');
// 重新建立连接
reconnectWebSocket();
});

完整生命周期管理类

type LifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated';
type LifecycleCallback = (state: LifecycleState, prevState: LifecycleState) => void;

class PageLifecycle {
private state: LifecycleState;
private callbacks: Set<LifecycleCallback> = new Set();

constructor() {
this.state = this.getState();
this.addEventListeners();
}

private getState(): LifecycleState {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
}

private addEventListeners(): void {
// 焦点变化
window.addEventListener('focus', () => this.updateState());
window.addEventListener('blur', () => this.updateState());

// 可见性变化
document.addEventListener('visibilitychange', () => this.updateState());

// 冻结/恢复
document.addEventListener('freeze', () => this.setState('frozen'));
document.addEventListener('resume', () => this.updateState());

// 卸载
window.addEventListener('pagehide', (event) => {
if (!event.persisted) {
this.setState('terminated');
}
});
}

private updateState(): void {
this.setState(this.getState());
}

private setState(newState: LifecycleState): void {
if (newState !== this.state) {
const prevState = this.state;
this.state = newState;
this.callbacks.forEach((callback) => callback(newState, prevState));
}
}

// 订阅状态变化
subscribe(callback: LifecycleCallback): () => void {
this.callbacks.add(callback);
return () => this.callbacks.delete(callback);
}

// 获取当前状态
getState(): LifecycleState {
if (document.visibilityState === 'hidden') {
return 'hidden';
}
if (document.hasFocus()) {
return 'active';
}
return 'passive';
}
}

// 使用
const lifecycle = new PageLifecycle();

lifecycle.subscribe((state, prevState) => {
console.log(`状态变化: ${prevState} -> ${state}`);

switch (state) {
case 'active':
startAnimations();
break;
case 'passive':
// 可能需要降低更新频率
break;
case 'hidden':
pauseAnimations();
saveState();
break;
case 'frozen':
// 清理资源
break;
}
});

性能优化应用

延迟加载非关键资源

// 方法 1: DOMContentLoaded 后加载
document.addEventListener('DOMContentLoaded', () => {
// 延迟加载非关键 CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/styles/non-critical.css';
document.head.appendChild(link);
});

// 方法 2: load 后加载
window.addEventListener('load', () => {
// 延迟加载分析脚本
const script = document.createElement('script');
script.src = '/analytics.js';
document.body.appendChild(script);
});

// 方法 3: 空闲时加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
loadNonCriticalResources();
});
}

智能预加载

// 页面可见时预加载
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 预加载可能需要的资源
const links = document.querySelectorAll<HTMLAnchorElement>('a[data-preload]');
links.forEach((link) => {
const preloadLink = document.createElement('link');
preloadLink.rel = 'prefetch';
preloadLink.href = link.href;
document.head.appendChild(preloadLink);
});
}
});

常见面试问题

Q1: DOMContentLoaded 和 load 事件的区别?

答案

特性DOMContentLoadedload
触发时机DOM 树构建完成所有资源加载完成
等待资源不等待图片、CSS等待所有资源
触发顺序先触发后触发
常用场景DOM 操作、事件绑定获取元素尺寸、性能统计
document.addEventListener('DOMContentLoaded', () => {
// DOM 操作
});

window.addEventListener('load', () => {
// 获取图片尺寸等
});

Q2: 如何正确检测页面是否可见?

答案

// 使用 visibilitychange 事件
document.addEventListener('visibilitychange', () => {
const isVisible = document.visibilityState === 'visible';
console.log('页面可见:', isVisible);
});

// 也可以检查 document.hidden
if (document.hidden) {
console.log('页面不可见');
}

应用场景

  • 暂停视频播放
  • 停止动画
  • 暂停轮询请求
  • 统计页面停留时间

Q3: 如何阻止页面关闭?

答案

window.addEventListener('beforeunload', (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = ''; // 必须设置
}
});

注意

  • 现代浏览器会显示标准提示,忽略自定义消息
  • 不要滥用,只在有未保存数据时使用
  • 某些场景下浏览器可能忽略此事件

Q4: 什么是 bfcache?如何正确处理?

答案

bfcache(后退/前进缓存):浏览器缓存页面完整状态,后退时瞬间恢复。

// 使用 pageshow/pagehide 而非 load/unload
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
// 从 bfcache 恢复
refreshData();
}
});

window.addEventListener('pagehide', (e) => {
if (e.persisted) {
// 即将进入 bfcache
}
});

// ⚠️ 避免使用 unload,会阻止 bfcache

Q5: 如何安全地在页面卸载时发送数据?

答案

// ✅ 使用 navigator.sendBeacon
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const data = JSON.stringify({
event: 'page_leave',
duration: getPageDuration(),
});
navigator.sendBeacon('/analytics', data);
}
});

// ❌ 不要使用同步 XMLHttpRequest
// 会阻塞页面卸载,影响用户体验

相关链接