聊聊vue中keepalive的内存问题

广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买

聊聊vue中keepalive的内存问题

【相关推荐:Vuejs视频教程】

1.起因

最近发现公司项目偶发性发生 奔溃现象。

刚开始以为是代码写了一些死循环,检查完并未发现。

后面通过 performance 检查 发现内存飚到了1个多G, 可能是内存没有正常的回收,而项目是从多页面整合到单页面后发生的,单页面使用的是keepalive 内部页签实现。所以初步推断可能是内存挤爆了。

定位原因

通过performance -> memory 看到当前内存使用情况,

通过疯狂的打开内部页签+关闭,发现内存已经达到惊人的2g,相关的操作已经开始无法响应,页面卡顿甚至白屏

通过命令可以看到 可以使用2g,已使用2g, 封顶4g

神奇的是2g后,等一会依然可以继续操作,继续撸,内存已经怼到4g了

这时候已经芭比Q了,控制台console.log回车后,都没空间执行和输出了

2. 定位问题

1.还原场景

由于内部系统代码复杂并有交叉逻辑和隐性的内存泄露的代码。对比了公司其他内置多页签缓存项目,也存在类似问题。所以需要搭建一个纯净的环境一步步从底层分析。首先还原项目使用的版本环境。

2.写个demo

先写个demo重现问题。使用vue-cli创建项目对应版本 vue2.6.12, vue-router3.6.4

main.js

import Vue from 'vue'import App from './App.vue'import router from './router'Vue.config.productionTip = falsenew Vue({  render: h => h(App),  router}).$mount('#app')
登录后复制

App.vue

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>
登录后复制

view/index.vue

<template>    <div>首页</div></template><script>export default {    name:'index',}</script>
登录后复制

view/a.vue

<template>    <div>组件view<input v-model="myname"/> </div></template><script>export default {    name:'A',    data(){        return {            a:new Array(20000000).fill(1),//大概80mb            myname:""        }    },    mounted(){          this.myname = this.$route.query.name    }}</script>
登录后复制

router/index.js

import Vue from 'vue'import Router from 'vue-router'import a from '../view/a.vue'Vue.use(Router)const router = new Router({    mode: 'hash',      routes: [        {            path: '/',            redirect: '/index'        },        {            path: '/index',            component: () => import('../view/index.vue')        }    ]})//动态添加路由router.$append = (index) => {     router.addRoute(`a${index}`,{        path: `/a${index}`,        component:  {            ...a,            name: `a${index}`        },    })  }router.$push = (index) => {         router.push({            path:`/a${index}`,            query:{                name:`a${index}`            }        })} export default  router
登录后复制

demo效果

点击新增会创建一个80mb的组件,可以看到新增4个组件,keepalive占用大概330mb左右,(实时监控和performance接口计算,内存诊断报告会有偏差)

点击删除会默认移除最后一个元素,也可以通过元素上的x来删除,每次删除都先跳回到首页, 确保删除的view 不是正在显示的view。

3.重现问题

1.当创建4个组件后,删除最后一个a4时候,同时立即回收内存,内存并没有释放。依然是328mb。

2.但是当再删除多一个a3的时候 居然又释放的80,让人更加疑惑。

3.这还不算,如果我新增4个,然后先删除最前面的居然能实时的释放

好家伙,vue官方api也这么不靠谱吗?对于程序员来说,不确定问题比实实在在的错误都要难得多。

赶紧上官网看了下,发现vue2 从2.6.12 到 2.7.10 之间 在 2.6.13 修复了 关于keepalive的问题,由于2.7.10使用ts重写了,并且引入的vue3的compositionAPI,为了稳定,只升级到 2.6的最新2.6.14。

结果问题依然存在,于是又试了下2.7.10,结果还是一样的现象。

4.分析

4.1全局引用是否正常释放

在vue里,只有一个子节点App,再里面就是 keepalive 和 a1,a2,a3,a4 ,这5个是平级的关系

可以看到当删除a4的时候App里面的子节点只剩下keepalive 和 a1,a2,a3, 4个元素,所以这里没有内存问题。

4.2keepalive 的cache是否正常释放

可以看到cache集合里面已经移除a4的缓存信息

4.3挨个组件检查引用关系

通过诊断报告搜索vuecomponent,可以看到有7个vuecomponent的组件(keepalive 和 App.vue + index.vue + 自定义创建的4个动态a组件)

通过鼠标移动到对应的vueVomponent上会显示对应的实例,如下图的a4实例

现在我尝试删除a4,再生成报告2,在报告2中我们还是能看到a4,这时候内存就没有正常释放了

并且发引用关系已经变成11层,与其他的5层不一样。点击改a4后,下面Object页签会展开显示正在引用他的对象

鼠标移动到$vnode上看,发现居然是被a3组件引用了,这是为什么?

根据一层层关系最后发现

 a3组件.$vnode.parent.componentOptions.children[0] 引用着 a4
登录后复制

导致a4 无法正常释放

基于这个点,查询了前面a2,a3 也存在引用的关系,a1 正常无人引用它。

a2组件.$vnode.parent.componentOptions.children[0] 引用着 a3a1组件.$vnode.parent.componentOptions.children[0] 引用着 a2a1组件 正常,没被引用
登录后复制

这里看到看出 a3组件.$vnode.parent 其实就是keepalive对象。

由于keepalive不参与渲染,但是每次组件渲染都会传入componentOptions,componentOptions里面包含了当前的keepalive的信息,keepalive又包裹了上一次第一个渲染的子节点。

5.结论

当加载组件a1,a1对应的keepalive的componentOptions的children[0]信息也是a1。

当加载组件a2,a2对应的keepalive的componentOptions的children[0]信息也是a2,但是这时候上面的a1对应的keepalive由于是同一个引用,导致a1对应的keepalive的componentOptions信息也是a2。

当加载组件a3,a3对应的keepalive的componentOptions的children[0]信息也是a3,导致a2对应的keepalive的componentOptions信息也是a3。

当加载组件a4,a4对应的keepalive的componentOptions的children[0]信息也是a4,导致a3对应的keepalive的componentOptions信息也是a4。

上面描述的各个组件的引用关系,a1-> a2 -> a3 -> a4 。这也解释了为什么删除a1内存能够立即释放,同理继续删除a2 也是能正常释放。

但是如果先删除a4,由于a3引用着他所以不能释放a4。

3. 修复问题

1.思路

根据上面的关系我们指导,所有问题都是vue实例的时候关联的keepalive引用了别的组件,我们只需要把keepalive上面componentOptions的children[0] 引用的关系切断就ok了。这时候我们可以从vue的keepalive源码入手调整。

2.构建可以定位具体源码的环境

该项目使用的是vue 的cdn引入,所以只需要重新上传一份支持sourcemap的并且没有被混淆的vue库即可。通过--sourcemap 命令参数 生产支持源码映射的代码,以相对路径的方式上传的对应的cdn地址。参考地址

git clone --branch 2.6.14  https://github.com/vuejs/vue.git //拉取代码
登录后复制

修改package.json,添加 --sourcemap

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
登录后复制

本地运行

npm run dev
登录后复制

通过live server启动服务

这样每次修改源码,都会实时发布到dist下的vue.js 我们就可以实时调试了访问地址:访问地址:http://127.0.0.1:5500/dist/vue.js

3.改造现有项目成cdn

vue.config.js

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>0
登录后复制

public/index.html

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>1
登录后复制

这里cdn改成生成自己生成的vue sourcemap 实时地址。

4.调试代码

在开发者工具里,crtl+p 打开源码搜索框,输入keepalive,找到对应的源码。

在render方法里打上断点,可以发现每当路由发送变化,keepalive的render方法都会重新渲染

打开源码

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>2
登录后复制

这里包含了整个keepalive的所有逻辑,

刚开始也以为是LRU的设置问题,测试后发现keepalive的数组都是能正常释放。

怀疑是max最大长度限制,解决也是正常。确保keepalive内部能正常释放引用后,就要想如何修复这个bug,关键就是把children设置为空

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>3
登录后复制

最合适的位置就在每次render的时候都重置一下所有错误的引用即可

代码如下,把错误引用的children设置为空

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>4
登录后复制

怀着喜悦的心情以为一切ok,运行后发现,a4依然被保留着。NND点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

于是在keepalive源码的render方法加入

<template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>5
登录后复制

整体代码如下

  render () {    const slot = this.$slots.default    const vnode: VNode = getFirstComponentChild(slot)         //修复缓存列表问题    for (const key in this.cache) {      const entry: ?CacheEntry = this.cache[key]      if (entry && vnode && entry.tag && entry.tag !== vnode.tag ) { //如果当前的缓存对象不为空 并且 缓存与当前加载不一样        entry.componentInstance.$vnode.parent.componentOptions.children = []        <template>    <div>        <div>keep-alive includeList:{{indexNameList}}</div>        <button @click="routerAdd()">新增(enter)</button> <button @click="routerDel()">删除(esc)</button> <button @click="gc()">强制垃圾回收(backspace)</button> <span  >内存已使用<b id="usedJSHeapSize"></b></span>        <div class="keepaliveBox">            <keep-alive :include="indexNameList">                <router-view />            </keep-alive>        </div>        <div class="barBox">            <div class="box" v-for="(index) in indexList" :key="index">                <span @click="routerClick(index)">a{{index}}</span>                <button @click="routerDel(index)" title="删除(esc)">x</button>            </div>        </div>    </div></template><script>export default {    name: "App",    data() {        return {            indexList: [],            usedJSHeapSize: ''        }    },    mounted() {        const usedJSHeapSize = document.getElementById("usedJSHeapSize")        window.setInterval(() => {            usedJSHeapSize.innerHTML = (performance.memory.usedJSHeapSize / 1000 / 1000).toFixed(2) + "mb"        }, 1000)        // 新增快捷键模拟用户实际 快速打开关闭场景        document.onkeydown = (event) => {            event = event || window.event;            if (event.keyCode == 13) {//新增                 this.routerAdd()            } else if (event.keyCode == 27) {  //删除                  this.routerDel()             } else if (event.keyCode == 8) {  //垃圾回收                  this.gc()             }        };    },    computed: {        indexNameList() {            const res = ['index']//            this.indexList.forEach(index => {                res.push(`a${index}`)            })             return res        }    },    methods: {        routerAdd() {            let index = 0            this.indexList.length > 0 && (index = Math.max(...this.indexList))             index++             this.indexList.push(index)            this.$router.$append(index)            this.$router.$push(index)        },        routerDel(index) {             if (this.indexList.length == 0) return            if(!index) {                index = Math.max(...this.indexList)            }                 //每次删除都先跳回到首页, 确保删除的view 不是正在显示的view            if (this.$route.path !== '/index') {                 this.$router.push('/index')             }            let delIndex = this.indexList.findIndex((item) => item == index)            this.$delete(this.indexList, delIndex)            //延迟执行,加到下一个宏任务            // setTimeout(() => {            //     this.gc()             // }, 100);        },        routerClick(index) {            this.$router.$push(index)        },        gc(){            //强制垃圾回收 需要在浏览器启动设置 --js-flags="--expose-gc",并且不打开控制台,没有效果            window.gc && window.gc()        },     }};</script><style scoped>.keepaliveBox {    border: 1px solid red;    padding: 3px;}.barBox {    display: flex;    flex-wrap: wrap;}.box {    margin: 2px;    min-width: 70px;}.box>span {    padding: 0 2px;    background: black;    color: #fff;}</style>5      }    }   .....}
登录后复制

再次怀着喜悦的心情运行,发现这次靠谱了。

nice~~

总结

由于早期浏览器的架构都是一个页面html一个tab,所以很少会出现tab内存不够的情况。但是随着前端工程化的发展,单页面客户端渲染的应用也越来越普及。所以内存的问题也会日渐均增,对内存的优化与问题也会越来越多。

当遇到偶发的奔溃问题时候,chrome的内存工具是个很好的帮手,可以快速生成报告并告知你引用的嵌套关系。

分析问题还有一个好方法就是对比其他vue多页签项目是否存在内存泄露问题,结果发现一样存在。基于这个前提再去分析官方的代码。

官方源码其实也提供了好像的调试环境,配合sourcemap对于分析定位和调试源码问题非常关键。

当然改源码都是下策,最好的办法还是提issue。赶紧上githut 提个PR看看,从代码源头处理掉这个bug。

demo 源码地址github.com/mjsong07/vu…

issue地址github.com/vuejs/vue/i…

(学习视频分享:web前端开发、编程基础视频)

以上就是聊聊vue中keepalive的内存问题的详细内容,更多请关注9543建站博客其它相关文章!

广告:SSL证书一年128.66元起,点击购买~~~

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。

标签: Vue

作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:如何解决uni图标在app上不显示的问题
下一篇:怎样写css

发表评论

关闭广告
关闭广告