背景
背景是这样的,咱们应用vue2
开发一个在线客服应用的IM利用,根本布局是右边是访客列表,左边是访客对话,为了让对话加载更敌对,咱们将对话的路由应用<keep-alive>
缓存起来。然而如果将所有对话都缓存,未必会造成缓存过多卡顿的问题。天然,就应用上了<keep-alive>
提供的max
属性,设置一个缓存对话内容组件下限,依照LRU
算法,会销毁最旧拜访的组件,保留最近应用的组件。本认为美妙如期而至,直到上线后翻大车了,实在对话量大了,内存飙升卡顿。起初具体分析内存增长点,通过vue
的devtool
查看组件树,发现对话内容组件始终是递增,并非维持在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 = index this.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中<keep-alive>组件实现原理
为什么缓存雷同名称的组件,max
属性会生效呢?这里就要从Vue2
中<keep-alive>
组件实现原理来看。
<keep-alive>LRU算法
vue
会将VNode
及组件实例(componentInstance
)存到缓存(cache
),cache
是一个Object
,同时还会保护一个keys
队列;- 依据
LRU
算法对cache
和keys
的治理:以后激活组件已存在缓存中,将组件对应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)}}
<keep-alive>清理缓存函数实现
上面再来看清理缓存函数
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);}
看到这里仿佛也没有故障,到底是哪里出问题了呢?
<keep-alive>源码调试发现问题
无妨咱们打印一下
cache
(VNode
缓存)和keys
发现也没什么问题,依照URL
算法失去正确后果。
再看看清理缓存函数里cached$$1.tag
和current.tag
的打印
假相了!他两因为组件名称雷同,导致相等,没有进入销毁组件实例的判断里,这就是问题起源!为什么针对雷同组件名称不去销毁实例呢?可能是为了某些情景下组件复用吧。解决方案
既然问题症结咱们曾经找到,从源头下来解决问题当然最佳,然而事实是
vue2
源码层面是没有去解决的(vue3
有解决,这个前面再说),只能从咱们利用侧再去想想方法。这里我想到的有两种计划。计划一:剪枝法
保护一个全局状态(比方
vuex
)对话ids
队列,最大长度为max
,相似vue
中LRU
算法中的keys
,在组件activated
钩子函数触发时更新ids
队列。对话内容组件的子组件判断以后对话id
是否在ids
队列中,不在那么就会v-if
剔除,否则缓存起来,这样很大水平水平上开释缓存。相似剪去树的枝丫,加重分量,这里叫做「剪枝法」好了。计划二:自定义清理缓存函数
咱们不再应用
keep-alive
提供的max
属性来清理缓存,让其将组件实例全副缓存下来,以后激活组件,activated
钩子函数触发,此时通过this.$vnode.parent.componentInstance
获取组件实例,进而能够获取挂载在下面的cache
和keys
。这样咱们就能够通过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 += 1 if (cacheLen > max) { const key = keys.shift() cache[key].componentInstance.$destroy() cache[key] = null } }})},
上面对照
vue
的devtool
工具查看成果
完全符合预期!形式二从<keep-alive>
组件根就清理了缓存组件,更彻底,对业务代码侵染性也更小。
你认为这样就完了?下面我还提到在vue3
中曾经解决了这个问题。vue3中<KeepAlive>组件实现原理
话不多说,先来看下面雷同的案例在应用
vue3
写的成果如何呢?这里就不“反复”贴代码了,间接看devtool
组件树的体现。
没有冗余缓存组件,奈斯!vue3中<KeepAlive>LRU算法
vue3
中LRU
算法实现思路一样,只不过cache
和keys
别离应用Map
和Set
数据结构实现,数据更洁净简洁。const cache = new Map();const keys = new Set();// ...if (cachedVNode) { // copy over mounted state vnode.el = cachedVNode.el; // ... // make this key the freshest keys.delete(key); keys.add(key);}else { keys.add(key); // prune oldest entry if (max && keys.size > parseInt(max, 10)) { pruneCacheEntry(keys.values().next().value); }}
vue3中<KeepAlive>清理缓存函数实现
vue3
中清理组件实例缓存函数也是pruneCacheEntry
,不同的是,比对以后传入组件和在缓存中的组件tag
是否雷同,决定是否销毁组件实例。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.type
和current.type
到底是什么
比照咱们会发现,不再是简略的组件名称字符标记,而是一个对象形容,蕴含了很多属性,因为在初始化组件实例时,会给每个实例加上属性:props
、render
、setup
、__hmrId
等。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;}}
即便是对象中所有属性雷同,然而对象不是同一个援用地址,造成
cache.type
和current.type
不相等,因而会销毁实例对象unmount(cached)
。以上就是vue3
对这个问题解决方案。总结
最初,在
vue2
中会呈现<keep-alive>
缓存雷同名称组件,max
生效的问题,举荐应用自定义清理缓存函数,在获取组件实例根底上,对缓存实例销毁。下图是我在实在我的项目中优化的成绩。完~