1. 冷跳转 (window.location.href) 的数据存活状态
概念:类似于浏览器的"断电重启"或"物理刷新"。跳转后,浏览器会完全卸载当前页面的上下文,并重新加载新页面。
- 被彻底清空的(无法存活) :
- 所有存在"运行内存(JS Heap)"里的东西。
- JS 声明的普通变量(
let,const,var)、局部闭包变量。 - JS 模块闭包(Module Cache)及其内部变量 。例如写在
.vue文件<script>顶层、export default {}外部的变量:let count = 0; let timer = null;。 - 整个
window对象会被彻底销毁并替换 (浏览器会给新页面赋予一个全新的、原生的window,旧window及挂载在上面的所有自定义属性全部清空)。 - 框架状态(如 Vue 组件实例的状态,以及 Vuex 和 Pinia 里面的所有数据)。
- DOM 树及绑定的原生事件。
- 未执行完的定时器(
setTimeout、setInterval)。
- 存活下来的(不受跳页影响) :
- 写入"磁盘/硬盘"或由浏览器底层接管的持久化数据。
sessionStorage(只要页签不关就一直存在)。localStorage、Cookie。- 浏览器的 HTTP 静态资源缓存(强缓存/协商缓存)。
- URL 上的 Query 参数。
2. 热跳转 (Vue router.push) 的数据存活状态
概念 :单页面应用(SPA)内的无刷新跳转。本质是利用 History API 拦截跳转,仅仅是更换了页面上渲染的组件,JavaScript 执行环境没有被销毁(window 对象也没有被销毁)。
- 被销毁的(无法存活) :
- 旧页面的 Vue 组件实例本身(触发
beforeDestroy/unmounted钩子)。 - 该组件内部的
data、computed等局部状态。 - 该组件对应的 DOM 节点及组件级事件。
- 旧页面的 Vue 组件实例本身(触发
- 依然存在的(存活下来) :
window对象及其上的所有全局变量。- JS 模块闭包(Module Cache)及其内部变量 。即声明在
.vue或.js文件顶层、export default {}外部的模块级变量(例如:let count = 0; let timer = null;)。由于模块在一生中只解析一次,这些变量会像全局单例一样永远驻留内存。 - Vuex / Pinia 的全局状态树。
- 未清理的异步任务 (如
setTimeout,跳页后依然会在后台倒计时并在新页面触发回调,极易引发 Bug)。
3. 为什么大型混合应用(如本项目)大量使用 window.location.href?
在 Vue 项目中放弃丝滑的热跳转,转而使用暴力的冷跳转,主要基于以下架构和业务痛点:
- 移动端客户端拦截限制(最核心原因) : 在 iOS/Android 内嵌 H5 (Hybrid App) 的场景下,客户端需要拦截 H5 的跳转请求以注入原生功能(如拦截带特定参数的 URL 以唤起扫一扫、支付,或改变原生导航栏)。Vue 的
router.push不会发出真实的 HTTP 跳转请求,导致原生客户端(WebView)无法感知、无法拦截,从而导致大量端侧交互功能瘫痪。 - 微前端与多工程物理隔离 : 庞大的业务系统往往被拆分成多个独立的 Vue 项目(如商城独立部署,个人中心独立部署)。系统间的跨域跳转超出了单一
Vue Router的管辖范围,必须通过window.location.href跨系统跳跃。 - 微信 JS-SDK 签名与兼容性: 在微信 Webview 中,SPA 的无刷新路由变化极易导致 URL 签名错乱(尤其是 iOS 微信),进而导致分享、支付等 SDK 功能失效。
- 防范内存泄漏: 在重度电商长列表、多媒体场景下,长期的热跳转会导致内存积压。冷跳转相当于强制垃圾回收(GC),保障低端机型的稳定性。
4. 单页应用 (SPA) 中的网络请求与"幽灵回调"
在 router.push 热跳转模式下,如果旧页面发起了网络请求且未完成时发生了页面跳转,会产生以下现象:
- 请求依然在发送 :网络请求(如 Axios/Fetch 发起的 XMLHttpRequest 或 Fetch 动作)是由浏览器底层 API 代理的,并不依附于 Vue 组件实例。因此,跳页后该请求在后台继续存在并执行,直到服务器返回结果。
- 回调诈尸(幽灵回调) :当请求成功返回时,原先写在旧页面的
.then()回调函数会继续执行。 - 潜在危害 :如果回调中存在对全局状态(Vuex)的修改,或者使用了已经被销毁的
this实例,会导致状态意外篡改、报错甚至页面崩溃。 - 解决方案 :在 SPA 开发规范中,必须在组件销毁钩子(
unmounted/beforeDestroy)中,利用AbortController或框架提供的 CancelToken,手动取消尚未完成的网络请求。
5. 模块级变量(闭包)在冷热跳转中的差异案例
为了更直观地理解,我们来看一段日常开发的源代码,以及它被打包后的底层形态。
1. 日常开发的 Vue 源代码:
javascript
<script>
// ==== 危险地带:写在 export default 外面的模块级变量 ====
let count = 0;
let timer = null;
export default {
data() {
return {
// ==== 安全地带:写在组件实例内部的变量 ====
localCount: 0
}
},
mounted() {
count++;
this.localCount++;
console.log('外部 count:', count, '内部 localCount:', this.localCount);
}
}
</script>
2. 经过 Webpack/Vite 打包后的真实形态(底层的闭包机制):
javascript
// Webpack 在底层会将上面的代码包裹在一个独立的模块工厂函数中
function 模块工厂_组件A(module, exports) {
// 您的 count 和 timer 被关在这个模块的专属闭包里
let count = 0;
let timer = null;
// 导出您的 Vue 组件配置对象
module.exports = {
data() { return { localCount: 0 } },
mounted() {
count++;
this.localCount++;
}
}
}
- 背后的机制(Webpack Module Cache) : 浏览器执行时,Webpack/Vite 会维护一个模块缓存表(Module Cache) 。一个模块(即上述的模块工厂函数)在一生中只会被执行一次,随后其导出结果会被缓存在内存中。写在最外层的变量,实际上就是存在于这个模块工厂的闭包中,成为了事实上的"全局单例"。
- 在热跳转 (
router.push) 中 : 由于window和 JS 内存未被销毁,Webpack 的模块缓存表依然存在。当用户再次跳回该页面时,Webpack 查表发现它执行过了,于是不会重新执行 模块工厂代码。因此,闭包中的count和timer会依然存在并保持旧值。这极易导致计数器疯狂累加、或后台存在无数个未被清理的幽灵定时器(引发严重内存泄漏)。 - 在冷跳转 (
window.location.href) 中 : 整个页面"断电重启",Webpack 的模块缓存表被彻底撕毁。当新页面加载时,模块会被视为首次引入,重新执行闭包代码,count和timer会被彻底销毁并重新初始化,回归最初干净的状态。