起因
近期客户经常反馈系统崩溃的问题,尤其是在下午最频繁,经过自己的自测,发现系统tab关闭后内存并没有回收掉,目前我已经处理了,tab页签关闭后,手动清理keep-alive内的缓存,应该不存在内存泄漏的情况,看来还有其他地方的缓存没有清理掉。
定位问题
1.还原场景
公司项目是单页应用,所有的操作都在一个浏览器页签内操作,整个页面是通过Layou+子路由的方式布局的,路由层级达到4级,业务复杂繁琐。需要重新搭建一个纯净项目还原场景
2.写个demo
使用vue-cli创建项目,vue@2.7.9
,vue-router@3.6.5
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router
}).$mount('#app')
App.vue
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "App",
};
</script>
view/Page1、view/Page2、view/A1、view/A2
<template>
<div>
Page1
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "Page1",
};
</script>
<template>
<div>
Page2
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "Page2",
};
</script>
<template>
<div>组件view A1</div>
</template>
<script>
export default {
name: "A1",
data() {
return {
a: new Array(20000000).fill(1), //大概80mb
};
},
};
</script>
<template>
<div>组件view A2</div>
</template>
<script>
export default {
name: "A2",
data() {
return {
a: new Array(20000000).fill(1), //大概80mb
};
},
};
</script>
view/Layout.vue
<template>
<div>
<h1>Layout</h1>
<div class="box">
<p>二级路由</p>
<router-link :to="{ name: 'A' }">A</router-link>
<br />
<router-link :to="{ name: 'B' }">B</router-link>
</div>
<div class="box">
<p>三级路由</p>
<router-link :to="{ name: 'AA' }">Page1</router-link>
<br />
<router-link :to="{ name: 'BB' }">Page2</router-link>
</div>
<div class="box">
<button @click="includeRemove()">清理keepalive缓存</button>
<br />
<router-link to="/home">Home</router-link>
<br />
</div>
<h1>keep-alive</h1>
缓存页面:{{ include }}
<keep-alive :include="include">
<router-view ref="alive"></router-view>
</keep-alive>
</div>
</template>
<script>
export default {
name: "Layout",
data() {
return {
include: [],
};
},
watch: {
'$route'(val) {
const name = val.meta.name
if (name && !this.include.includes(name)) {
this.include.push(name);
}
}
},
methods: {
includeRemove() {
this.include = [];
},
},
mounted() {},
};
</script>
<style>
.box {
margin-bottom: 20px;
}
</style>
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
mode: "hash",
routes: [
{
path: "/",
redirect: "/home",
component: () => import("../view/Layout.vue"),
children: [
{
path: "home",
component: () => import("../view/Home.vue"),
},
],
},
{
path: "/a",
component: () => import("../view/Layout.vue"),
children: [
{
path: "a",
name: "A",
meta: {
name: 'A1'
},
component: () => import("../view/A1.vue"),
},
],
},
{
path: "/b",
component: () => import("../view/Layout.vue"),
children: [
{
path: "b",
name: "B",
meta: {
name: 'A2'
},
component: () => import("../view/A2.vue"),
},
],
},
{
path: "/a",
component: () => import("../view/Layout.vue"),
children: [
{
path: "page1",
name: "Page1",
component: () => import("../view/Page1.vue"),
children: [
{
path: "a",
name: "AA",
meta: {
name: 'Page1'
},
component: () => import("../view/A1.vue"),
},
],
},
],
},
{
path: "/b",
component: () => import("../view/Layout.vue"),
children: [
{
path: "page2",
name: "Page2",
component: () => import("../view/Page2.vue"),
children: [
{
path: "b",
name: "BB",
meta: {
name: 'Page2'
},
component: () => import("../view/A2.vue"),
},
],
},
],
},
],
});
export default router
运行效果
3.重现问题
可以看出,初始情况下,内存使用7.7MB左右
1.点击A、B后,内存占用168MB
2.点击Home,保证在清理缓存时,路由不占用A,B组件,再次点击清理keepalive缓存
手动GC后,发现内存使用变为7.9MB,说明A、B组件成功释放掉了,这个模拟了公司项目前期只有二级路由的情况,那个时候还不存在系统崩溃的问题,这里也刚好印证了。
3.点击Page1、Page2,内存占用是168MB
4.点击Home,再次点击清理keepalive缓存
这个时候就出问题了,内存并没有成功的释放掉,问题找到了
4.分析
首先记录初始情况下内存占用
打开Page1、Page2,切换到Home页面,清理keepalive缓存,记录当前内存快照
从图中可以看出,A1组件还存在,并且是vue-router引用了,nameMap保存了所有的路由信息,这样的话问题就找到了
初始状态下路由信息
打开Page1、Page2,切换到Home页面,清理keepalive缓存后路由信息
只有Page1、Page2的instances.default
是undefined,A1,B1还保留了组件实例
测试另外一种情况,清理keepalive缓存前不切换到Home,这种情况下,内存是可以成功释放掉的。
5.结论
- 如果是在当前路由关闭tab,然后清理keepalive缓存,内存是可以正常回收的
- 如果是在其他路由关闭非激活的路由时,二级路由组件可以正常回收,二级以下路由内存回收异常,猜测非激活路由matched内的信息以变更了,毕竟是单例模式,这就说的通,为啥激活的路由移除缓存是正常的了
3.修复问题
1.思路
- 获取关闭当前tab路由父子关系
- 通过所有的路由信息,遍历删除相关路由的
instances.default
值
2.具体代码实现
includeRemove() {
this.include = [];
// 为啥vue-router不开放直接获取nameMap的接口 淦
const routes = this.$router.getRoutes()
const nameMap = new Map()
for (let index = 0; index < routes.length; index++) {
const r = routes[index];
nameMap.set(r.name, r)
}
// 假设我这边获取到了当前移除的tab页签,具体代码根据具体项目实现
const rList = ['AA', 'BB']
for (let index = 0; index < rList.length; index++) {
const name = rList[index];
const r = nameMap.get(name)
if (r) r.instances.default = undefined
}
}
代码改造后重新按照流程走了一遍,内存使用情况如下,成功解决问题
总结
一开始根本无从下手,总觉得是客户操作问题,或者是电脑内存太小,一直没有太在意,当更多人反馈这个问题时,才意识到这个问题很普遍,必须得解决掉才行,从开始解决到已解决花了一个星期的时间,解决的代码很简单,但是解决问题的过程很艰辛。
感谢 juejin.cn/post/715318… 博主的文章,给我解决的思路和灵感
demo 源码地址 github.com/gaoyuanfell…