vue2中keepalive手动清理内存,存在子路由内存无法回收的问题

1,667 阅读4分钟

起因

近期客户经常反馈系统崩溃的问题,尤其是在下午最频繁,经过自己的自测,发现系统tab关闭后内存并没有回收掉,目前我已经处理了,tab页签关闭后,手动清理keep-alive内的缓存,应该不存在内存泄漏的情况,看来还有其他地方的缓存没有清理掉。

定位问题

1.还原场景

公司项目是单页应用,所有的操作都在一个浏览器页签内操作,整个页面是通过Layou+子路由的方式布局的,路由层级达到4级,业务复杂繁琐。需要重新搭建一个纯净项目还原场景

2.写个demo

使用vue-cli创建项目,vue@2.7.9vue-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

运行效果

image.png

3.重现问题

可以看出,初始情况下,内存使用7.7MB左右

1.点击A、B后,内存占用168MB

image.png

2.点击Home,保证在清理缓存时,路由不占用A,B组件,再次点击清理keepalive缓存

image.png

手动GC后,发现内存使用变为7.9MB,说明A、B组件成功释放掉了,这个模拟了公司项目前期只有二级路由的情况,那个时候还不存在系统崩溃的问题,这里也刚好印证了。

3.点击Page1、Page2,内存占用是168MB

image.png

4.点击Home,再次点击清理keepalive缓存

image.png

这个时候就出问题了,内存并没有成功的释放掉,问题找到了

4.分析

首先记录初始情况下内存占用

image.png

打开Page1、Page2,切换到Home页面,清理keepalive缓存,记录当前内存快照

image.png

从图中可以看出,A1组件还存在,并且是vue-router引用了,nameMap保存了所有的路由信息,这样的话问题就找到了 image.png

初始状态下路由信息

image.png

打开Page1、Page2,切换到Home页面,清理keepalive缓存后路由信息

image.png 只有Page1、Page2的instances.default是undefined,A1,B1还保留了组件实例

测试另外一种情况,清理keepalive缓存前不切换到Home,这种情况下,内存是可以成功释放掉的。

5.结论

  1. 如果是在当前路由关闭tab,然后清理keepalive缓存,内存是可以正常回收的
  2. 如果是在其他路由关闭非激活的路由时,二级路由组件可以正常回收,二级以下路由内存回收异常,猜测非激活路由matched内的信息以变更了,毕竟是单例模式,这就说的通,为啥激活的路由移除缓存是正常的了

3.修复问题

1.思路

  1. 获取关闭当前tab路由父子关系
  2. 通过所有的路由信息,遍历删除相关路由的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
  }
}

代码改造后重新按照流程走了一遍,内存使用情况如下,成功解决问题

image.png

总结

一开始根本无从下手,总觉得是客户操作问题,或者是电脑内存太小,一直没有太在意,当更多人反馈这个问题时,才意识到这个问题很普遍,必须得解决掉才行,从开始解决到已解决花了一个星期的时间,解决的代码很简单,但是解决问题的过程很艰辛。

感谢 juejin.cn/post/715318… 博主的文章,给我解决的思路和灵感

demo 源码地址 github.com/gaoyuanfell…