pagehide/beforeunload / unload / onUnmounted 执行顺序与navigator.sendBeacon使用陷阱详解
一、前言
在前端项目中,我们经常需要在用户 关闭页面、刷新页面或离开当前路由 时执行一些清理或上报操作,例如:
-
使用
navigator.sendBeacon()上报用户停留时间; -
清除定时器或全局事件;
-
保存用户未提交的表单数据。
在实践中,很多开发者容易混淆三个事件或钩子:
-
beforeunload -
unload -
Vue 的
onUnmounted
它们看似相似,但触发时机、作用范围和执行顺序都不同。本文将系统梳理它们的差异,并给出最佳实践。
二、事件执行顺序对比
当浏览器关闭或刷新页面时,事件触发顺序如下:
| 阶段 | 事件 / 钩子 | 说明 |
|---|---|---|
| ① | beforeunload | 页面即将卸载,可同步执行逻辑或调用 sendBeacon |
| ② | pagehide(可选) | 页面被隐藏或卸载,支持 bfcache |
| ③ | unload | 页面资源彻底卸载,JS 环境即将清空 |
| ❌ | onUnmounted | 不会触发(Vue 根组件还未卸载) |
而在 SPA 内部路由切换 时:
| 阶段 | 事件 / 钩子 | 说明 |
|---|---|---|
| ① | beforeRouteLeave | 路由离开前(可中断跳转) |
| ② | onUnmounted | 当前组件被销毁 |
| ❌ | beforeunload / unload | 不会触发,因为页面未关闭 |
三、window.onbeforeunload 与 addEventListener 的区别
很多开发者会直接写:
window.onbeforeunload = () => {navigator.sendBeacon('/report')
}
问题在于:在多个组件中使用时,后定义的会覆盖前一个。
// 组件 A
window.onbeforeunload = () => console.log('A')// 组件 B
window.onbeforeunload = () => console.log('B')
结果只会输出 B,因为后者覆盖了前者。
✅ 推荐写法:addEventListener
function handleBeforeUnload() {navigator.sendBeacon('/report')
}window.addEventListener('beforeunload', handleBeforeUnload)
-
支持多个监听器
-
不会互相覆盖
-
兼容性好
注意:在组件卸载时需要移除监听器,防止重复绑定或内存泄漏。
四、addEventListener 要配合 onUnmounted 移除
在 Vue 组件中:
onMounted(() => {const handleUnload = () => {console.log('unload')}window.addEventListener('unload', handleUnload)
})onUnmounted(() => {window.removeEventListener('unload', handleUnload)
})
这样可以确保 SPA 内部路由切换时不会产生多余的监听器。
五、刷新页面时 onUnmounted 不触发的原因
onMounted(() => {const handleUnload = () => console.log('unload')window.addEventListener('unload', handleUnload)
})onUnmounted(() => {window.removeEventListener('unload', handleUnload)
})
在刷新页面时,仍然会触发 unload。原因是:
-
刷新时 Vue 根实例尚未“优雅卸载”,浏览器直接进入页面卸载阶段。
-
因此
onUnmounted永远不会执行,监听器没有被移除,但unload仍然有效。
在 SPA 内部路由切换时,onUnmounted 会触发,从而移除监听器。
六、SPA 场景下的影响
| 场景 | 事件触发情况 | 说明 |
|---|---|---|
| 页面刷新 / 关闭 | ✅ unload 触发❌ onUnmounted 不触发 | 监听器仍有效 |
| SPA 内部路由切换 | ✅ onUnmounted 触发❌ unload 不触发 | 如果在 onUnmounted 中移除监听器,则 unload 不再生效 |
结论:如果你在
mounted中绑定了unload事件,并在onUnmounted中移除它,SPA 内路由跳转后该事件不会再触发。
七、使用 pagehide 代替 beforeunload / unload
现代浏览器提供了 pagehide 事件,推荐在页面退出埋点中使用:
window.addEventListener('pagehide', () => {navigator.sendBeacon('/report', JSON.stringify({ time: Date.now() }))
})
与 beforeunload / unload 对比
| 事件 | 标签页切换 | 页面刷新 | 页面关闭 | 后退 / 前进(同域 bfcache) | 后退离开当前站点(不同域) |
|---|---|---|---|---|---|
beforeunload | ❌ 不触发 | ✅ | ✅ | ✅ | ✅ 触发 |
unload | ❌ 不触发 | ✅ | ✅ | ⚠️(bfcache 下可能不触发) | ✅ 触发(页面真正卸载) |
pagehide | ❌ 不触发 | ✅ | ✅ | ✅ | ✅ 触发(可用 event.persisted=false 判断) |
- pagehide 覆盖了刷新、关闭、后退、bfcache 等场景
-
异步操作使用
navigator.sendBeacon()可以安全上报 -
SPA 内部路由切换仍需
onUnmounted做组件级清理
小结:在现代浏览器中,
pagehide已经是最可靠的页面退出事件,可以替代beforeunload和unload。
八、最佳实践:埋点或离开上报
onMounted(() => {const sendReport = () => {navigator.sendBeacon('/report', JSON.stringify({ time: Date.now() }))}window.addEventListener('pagehide', sendReport)window.addEventListener('beforeunload', sendReport) //兼容旧版本
})onUnmounted(() => {window.removeEventListener('pagehide', sendReport)window.removeEventListener('beforeunload', sendReport)
})
关键要点总结
| 要点 | 建议 |
|---|---|
| 1 | 不要使用 window.onbeforeunload,会被覆盖 |
| 2 | 使用 addEventListener,并在 onUnmounted 中移除 |
| 3 | onUnmounted 在刷新或关闭页面时不会触发 |
| 4 | SPA 内部路由切换时触发 onUnmounted,此时移除监听器 |
| 5 | 页面退出埋点推荐使用 pagehide + beforeunload,不依赖 unload |
九、总结
-
onUnmounted属于 Vue 生命周期,只在组件销毁(如 SPA 内路由切换)时触发。 -
beforeunload/unload属于浏览器生命周期,页面关闭或刷新时才触发。 -
pagehide是现代浏览器最可靠的页面退出事件,兼容 bfcache。 -
window.onbeforeunload会被覆盖,不推荐使用。 -
addEventListener更灵活,但 SPA 内路由切换时要在onUnmounted移除。 -
页面退出埋点(
sendBeacon)推荐放在pagehide或beforeunload,几乎覆盖所有退出场景。
