嘿,vue中keep-alive有个「大坑」你可能还不知道

背景

背景是这样的,我们使用vue2开发一个在线客服使用的IM应用,基本布局是左边是访客列表,右边是访客对话,为了让对话加载更友好,我们将对话的路由使用<keep-alive>缓存起来。但是如果将所有对话都缓存,未必会造成缓存过多卡顿的问题。自然,就使用上了<keep-alive>提供的max属性,设置一个缓存对话内容组件上限,按照LRU算法,会销毁最旧访问的组件,保留最近使用的组件。本以为美好如期而至,直到上线后翻大车了,真实对话量大了,内存飙升卡顿。后来具体分析内存增长点,通过vuedevtool查看组件树,发现对话内容组件一直是递增,并非维持在max设置的数量上限! 各位看官稍安勿躁,下面就具体分析造成这个「大坑」的原理,以及解决它的方案。

情景模拟

为了方便模拟背景案例,这里就用vue2简单的写一个demo。 对话列表组件 APP.vue,点击列表中的某个访客,加载与访客对话内容。

<template><div id="app"><section class="container"><aside class="aside"><ul><li :class="{ active: active === index }" v-for="(user, index) in userList" :key="index"@click="selectUser(index, user)">{{ user.name }}</li></ul></aside><section class="main"><keep-alive :max="3"><chat-content :key="currentUser.id" :user-info="currentUser"></chat-content></keep-alive></section></section></div>
</template>

<script> import ChatContent from './views/ChatContent.vue';
export default {components: {ChatContent},data() {return {active: -1,currentUser: {},userList: [{ id: 1, name: "张三" },{ id: 2, name: "李四" },{ id: 3, name: "王五" },{ id: 4, name: "老六" },{ id: 5, name: "老八" },{ id: 6, name: "老九" },]}},methods: {selectUser(index) {this.active = indexthis.currentUser = this.userList[index];}},
} </script> 

这里使用keep-alive组件包裹的对话内容组件,需要加上key唯一标志,这样才会缓存相同名称(不同key)的组件,否则不会缓存。 对话内容组件ChatContent.vue,简单加一个计数器验证组件缓存了。

<template><div><h2>{{ userInfo.name }}</h2><h3>{{ num }}</h3><button @click="increament">+1</button></div>
</template>

<script> export default {props: {userInfo: Object,},data() {return {num: 0,};},methods: {increament() {this.num += 1;},},
}; </script> 

情景模拟结果

实验发现,虽然缓存组件个数上限max为3,实际是逐个缓存了全部内容组件,看来设置max属性失效了。

Vue2中组件实现原理

为什么缓存相同名称的组件,max属性会失效呢?这里就要从Vue2<keep-alive>组件实现原理来看。

LRU算法

  • vue会将VNode及组件实例(componentInstance)存到缓存(cache),cache是一个Object,同时还会维护一个keys队列;
  • 根据LRU算法对cachekeys的管理:当前激活组件已存在缓存中,将组件对应key先删除,再插入的方式往前移动;
  • 当前激活组件没有再缓存中,直接存入缓存,此时判断是否超过了缓存个数上限,如果超过了,使用pruneCacheEntry清理keys第一个位置(最旧)的组件对应的缓存。
if (cache[key]) {vnode.componentInstance = cache[key].componentInstance;// make current key freshestremove(keys, key);keys.push(key);
} else {cache[key] = vnode;keys.push(key);// prune oldest entryif (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode);console.log('cache: ', cache)console.log('keys: ', keys)}
} 

清理缓存函数实现

下面再来看清理缓存函数pruneCacheEntry的实现:比对当前传入组件和在缓存中的组件tag是否相同,如果不同,就去销毁组件实例,否则不销毁。

function pruneCacheEntry (cache,key,keys,current
) {var cached$$1 = cache[key];if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {cached$$1.componentInstance.$destroy();}cache[key] = null;remove(keys, key);
} 

看到这里似乎也没有毛病,究竟是哪里出问题了呢?

源码调试发现问题

不妨我们打印一下cacheVNode缓存)和keys

解决方案

既然问题症结我们已经找到,从源头上去解决问题当然最佳,但是现实是vue2源码层面是没有去解决的(vue3有解决,这个后面再说),只能从我们应用侧再去想想办法。这里我想到的有两种方案。

方案一:剪枝法

维护一个全局状态(比如vuex)对话ids队列,最大长度为max,类似vueLRU算法中的keys,在组件activated钩子函数触发时更新ids队列。对话内容组件的子组件判断当前对话id是否在ids队列中,不在那么就会v-if剔除,否则缓存起来,这样很大程度程度上释放缓存。类似剪去树的枝丫,减轻重量,这里叫做「剪枝法」好了。

方案二:自定义清理缓存函数

我们不再使用keep-alive提供的max属性来清理缓存,让其将组件实例全部缓存下来,当前激活组件,activated钩子函数触发,此时通过this.$vnode.parent.componentInstance获取组件实例,进而可以获取挂载在上面的cachekeys。这样我们就可以通过LRU算法,根据key自定义精准清理缓存了。

activated() {const { cache, keys } = this.$vnode.parent.componentInstance;console.log('activated cache: ', cache)console.log('activated keys: ', keys)let cacheLen = 0const max = 3Object.keys(cache).forEach(key => {if (cache[key]) {cacheLen += 1if (cacheLen > max) {const key = keys.shift()cache[key].componentInstance.$destroy()cache[key] = null}}})
}, 

下面对照 vuedevtool工具查看效果

vue3中组件实现原理

话不多说,先来看上面相同的案例在使用vue3写的效果如何呢?这里就不“重复”贴代码了,直接看devtool组件树的表现。

vue3中LRU算法

vue3LRU算法实现思路一样,只不过cachekeys分别使用MapSet数据结构实现,数据更干净简洁。

const cache = new Map();
const keys = new Set();
// ...
if (cachedVNode) {// copy over mounted statevnode.el = cachedVNode.el;// ...// make this key the freshestkeys.delete(key);keys.add(key);
}
else {keys.add(key);// prune oldest entryif (max && keys.size > parseInt(max, 10)) {pruneCacheEntry(keys.values().next().value);}
} 

vue3中清理缓存函数实现

vue3中清理组件实例缓存函数也是pruneCacheEntry,不同的是,比对当前传入组件和在缓存中的组件type是否相同,决定是否销毁组件实例。

function pruneCacheEntry(key) {const cached = cache.get(key);if (!current || cached.type !== current.type) {unmount(cached);}else if (current) {// current active instance should no longer be kept-alive.// we can't unmount it now but it might be later, so reset its flag now.resetShapeFlag(current);}cache.delete(key);keys.delete(key);
} 

再来看看cache.typecurrent.type到底是什么

function initProps(instance, rawProps, isStateful, isSSR = false) {const props = {};const attrs = {};def(attrs, InternalObjectKey, 1);instance.propsDefaults = /* @__PURE__ */ Object.create(null);setFullProps(instance, rawProps, props, attrs);// ... instance.attrs = attrs;
}
function isInHmrContext(instance) {while (instance) {if (instance.type.__hmrId)return true;instance = instance.parent;}
} 

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue,使用`<keep-alive>`组件可以缓存组件的状态,使得在组件之间切换时保留组件的状态。当没有使用`<keep-alive>`组件时,每次切换到一个新的组件时,旧的组件会被销毁,下次再切换回来时需要重新创建。 下面是一个简单的例子来演示无`<keep-alive>`和有`<keep-alive>`页面来回切换的区别。 ```vue <template> <div> <button @click="toggle">Toggle</button> <!-- 无 keep-alive --> <div v-if="showComponent"> <ComponentA /> </div> <!-- 有 keep-alive --> <keep-alive> <div v-if="showComponent"> <ComponentB /> </div> </keep-alive> </div> </template> <script> import ComponentA from './ComponentA.vue' import ComponentB from './ComponentB.vue' export default { components: { ComponentA, ComponentB }, data() { return { showComponent: true } }, methods: { toggle() { this.showComponent = !this.showComponent } } } </script> ``` 在上述代码,我们有两个组件:`ComponentA`和`ComponentB`。当点击"Toggle"按钮时,`showComponent`的值会切换,控制两个组件的显示和隐藏。 如果没有使用`<keep-alive>`,每次切换时,旧的组件会被销毁,然后重新创建新的组件。这意味着组件的状态会丢失,所有的数据和计算属性都会被重置。 如果使用了`<keep-alive>`,在切换时旧的组件不会被销毁,而是被缓存起来。这样,切换回来时组件的状态会被保留,数据和计算属性等都会保持不变。 所以,使用`<keep-alive>`可以提高页面切换的性能,并且保留组件的状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值