本篇内容介绍了“vue中keepalive的内存问题怎么解决”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名申请、网页空间、营销软件、网站建设、龙门网站维护、网站推广。
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 = false
new Vue({
render: h => h(App),
router
}).$mount('#app')App.vue
keep-alive includeList:{{indexNameList}}内存已使用 a{{index}}
view/index.vue
首页
view/a.vue
组件view
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 routerdemo效果

点击新增会创建一个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] 引用着 a3 a1组件.$vnode.parent.componentOptions.children[0] 引用着 a2 a1组件 正常,没被引用
这里看到看出 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
module.exports = {
chainWebpack: config => {
config.externals({
vue: "Vue",
});
},
configureWebpack: {
devtool: "eval-source-map"
},
lintOnSave: false
};public/index.html
favicon.ico"><%= htmlWebpackPlugin.options.title %>
这里cdn改成生成自己生成的vue sourcemap 实时地址。
4.调试代码
在开发者工具里,crtl+p 打开源码搜索框,输入keepalive,找到对应的源码。

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

打开源码
/* @flow */
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type CacheEntry = {
name: ?string;
tag: ?string;
componentInstance: Component;
};
type CacheEntryMap = { [key: string]: ?CacheEntry };
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
function matches (pattern: string | RegExp | Array, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const entry: ?CacheEntry = cache[key]
if (entry) {
const name: ?string = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
const patternTypes: Array = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
} 这里包含了整个keepalive的所有逻辑,
刚开始也以为是LRU的设置问题,测试后发现keepalive的数组都是能正常释放。
怀疑是max最大长度限制,解决也是正常。 确保keepalive内部能正常释放引用后,就要想如何修复这个bug,关键就是把children设置为空
组件.$vnode.parent.componentOptions.children = []
最合适的位置就在每次render的时候都重置一下所有错误的引用即可
代码如下,把错误引用的children设置为空
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 = []
}
}
.....
}怀着喜悦的心情以为一切ok,运行后发现,a4依然被保留着。NND

点击后发现,是a4的dom已经没在显示,dom处于游离detach状态,看看是谁还引用着。好家伙,又是父节点keepalive的引用着,这次是elm。

于是在keepalive源码的render方法加入
entry.componentInstance.$vnode.parent.elm = null
登录后复制
整体代码如下
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 = []
entry.componentInstance.$vnode.parent.elm = null
}
}
.....
}再次怀着喜悦的心情运行,发现这次靠谱了。
“vue中keepalive的内存问题怎么解决”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!
当前文章:vue中keepalive的内存问题怎么解决
地址分享:http://www.jxjierui.cn/article/jhicso.html


咨询
建站咨询
