visibilitychange 事件必须绑定到 document 而非 window,且不冒泡;判断显隐需用 document.hidden 而非 event.type;iOS 存在触发延迟或漏发问题;SPA 中需避免重复绑定和内存泄漏。

visibilitychange 事件必须监听 document,不是 window
很多人在写 visibilitychange 时习惯性绑定到 window,但这个事件只在 document 上触发。绑错对象会导致监听完全失效,且无任何报错提示。
-
document.addEventListener('visibilitychange', handler)✅ 正确 -
window.addEventListener('visibilitychange', handler)❌ 不会触发 - 注意:该事件不冒泡,不能委托到父级元素
判断页面显隐要用 document.hidden,不是 event.type
事件名是 visibilitychange,但它的触发本身不带“当前是否可见”的状态信息。真正反映页面显隐状态的是 document.hidden 布尔值——true 表示页面被隐藏(如切到其他 tab、最小化浏览器),false 表示可见。
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面已隐藏');
} else {
console.log('页面已显示');
}
});
注意 Safari 和 iOS WebView 的兼容性陷阱
iOS 13+ Safari 和部分 WKWebView 环境中,document.hidden 在页面刚加载时可能为 false,但实际尚未渲染完成;更关键的是:当用户从后台切回 App 时,visibilitychange 有时延迟触发或漏发。
- 不要依赖首次进入页面时的
document.hidden值做初始化逻辑 - 对关键行为(如暂停视频、停止心跳)建议加一层兜底:结合
pagehide/pageshow事件 - Android Chrome 和桌面端主流浏览器基本无此问题
避免重复绑定和内存泄漏
在单页应用(SPA)中,如果组件反复挂载/卸载(比如 React useEffect 或 Vue onMounted),容易多次绑定同一事件却未清理,导致 handler 被执行多次。
立即学习“前端免费学习笔记(深入)”;
- 每次绑定前先用
removeEventListener清理旧监听(需保存 handler 引用) - 或使用一次性绑定 + 组件销毁时解绑的模式
- 不要在内联函数里绑定:
document.addEventListener('visibilitychange', () => {...})→ 无法解绑
const handleVisibilityChange = () => {
// ...
};
// 绑定
document.addEventListener('visibilitychange', handleVisibilityChange);
// 解绑(例如在组件 unmount 时)
document.removeEventListener('visibilitychange', handleVisibilityChange);
页面显隐状态看似简单,但 document.hidden 的初始值、iOS 平台的事件时机、以及 SPA 中的监听生命周期,三者叠加最容易出隐蔽问题。











