vue2之keep-alive详解
起因
我有个朋友,在维护公司的项目时候,出现了一个bug,就是使用keepalive的页面居然有时候没有缓存效果。真是奇了个大怪,下面是问题代码。于是本着助人为乐的好品质,我就帮他看了一下,发现事情不简单
keep-alive是什么
首先在开始前,我们要先了解一下keep-alive这个组件的基本用法。 我这边就长话短说。这个组件的功能只有一个那就是缓存组件,可以让其包裹的组件不销毁,起到一个缓存的作用。
接受可选的三个参数
- max 缓存的最大组件数量,支持数字和字符串
- include 需要缓存的数据,支持字符串,数组,正则
- exclude 不需要缓存的数据,支持字符串,数组,正则 具体的详细介绍可以看这里 keep-alive官方介绍
寻找bug
了解了基本用法,那就要去定位问题了
会不会是include的值匹配的范围太小,导致无法缓存?
会不会是include
的值让其无法缓存呢?于是我让我的那个朋友打印了一下include
中的值,好家伙返回的是组件name属性组成的数组,那应该不是这个问题了,因为需要缓存的组件在这个数组里
会不会exclude的值匹配的范围太大,导致无法缓存?
不是include造成的那应该就是exclude
这个数据太大了,导致其无法缓存。于是我又让我那个朋友打印了一下exclude
。结果出人意料,居然是个空数组。我的天啊。这是啥原因?
我看了一下组件可以接受max
这个参数,是不是组件内部默认设置了最大缓存数,导致缓存失败?
keep-alive内部是否设置了默认max?
对于这个疑问,本着解决问题的态度,于是我把vue源码拉取下来,准备一探究竟
源码路径src =>core => components => keep-alive.ts github:github.com/vuejs/vue/b… 我这边看的是2.7版本的,好像之前的flow语法全给用ts改写了,不得不说vue团队还是挺强大的
怀着好奇心情看源码
max是不是设置了默认值
于是我打开了源码keep-alive.ts,在当前文件搜了一下max
。有两处引用,一次是props相关,一次是使用相关
- 这是定义props的地方,可以看出来,这里没有做默认值处理,只是定义了接受传入的props
- 继续看使用的地方,好像也没有默认值处理啊,只是判断max是否定义,和超出他才调用
pruneCacheEntry
这个方法
pruneCacheEntry做了什么
那么pruneCacheEntry
是什么,他做了什么呢,从变量名,可以大概猜出,这个去除缓存的函数
我这边把函数改成js,方便大家阅读
看得出,他接受了四个参数,分别是cache,key,keys,current
current看起来是当前keep-alive
组件的虚拟dom,那cache
,keys
是什么呢,从哪里定义的呢
cache,keys是什么呢
于是我找到了这两个变量的声明地方。原来在created
这个生命周期中声明了
- cache 是一个空对象
- keys 是一个空数组 从字面意思可以看出,这两个是缓存用的,具体他是咋用的?
初始化
原来在组件初始化的时候,通过mounted
这个生命周期调用了cacheVNode
这个方法,这个方法拥有缓存的奇效
具体实现如下
好家伙这里实现和之前调用max的地方居然是同一个函数
- 取出组件中的
vnodeToCache
和keyToCache
, - 从
vnodeToCache
中获取的组件名tag,组件实例和选项 - 将组件存放在缓存中
- 并把匹配的key值推入
keys
尾部 - 清空当前的
vnodeToCache
原来如此,那么vnodeToCache
和keyToCache
从哪里定义的呢
vnodeToCache和keyToCache
我在render中找到了定义,为了解读方便,我把ts改成了js,并添加了点注释
render() {
const slot = this.$slots.default;
const vNode = getFirstChildren(slot);
const componentsOptions = vNode.componentOptions;
// 获取第一个有效组件的配置项
if (componentsOptions) {
const { exclude, include } = this;
const name = getComponentName(componentsOptions);
//如果传递了限制 这里如果没有命中规则,直接返回
if ((include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name))) {
console.log('不符合缓存条件,直接返回');
return vNode;
}
console.log(componentsOptions);
const { cache, keys } = this;
// 这里获取组件的key,如果用户没有传入key,则使用组件的cid+组件的名称作为key
const key =
vNode.key == null
? componentsOptions.Ctor.cid + (componentsOptions.tag ? `::${componentsOptions.tag}` : '')
: vNode.key;
if (cache[key]) {
console.log('缓存命中');
// 将命中的缓存组件拿出来覆盖当前vNode的componentInstance
console.log(cache[key]);
vNode.componentInstance = cache[key].componentInstance;
// 由于需要将当前组件变成最新被使用
// 1.先删除命中的key
remove(keys, key);
// 2.重新追加到尾部,这样可以保证这个组件是最近被使用
keys.push(key);
} else {
console.log('未命中缓存,将其加入缓存', key);
this.vNodeToCache = vNode;
this.keyToCache = key;
}
vNode.data.keepAlive = true;
}
return vNode;
}
原来是这样,大致流程如下
- 首先通过slot获取到了当前默认插槽里的组件
- 通过
getFirstChildren
获取到了正在渲染的第一个组件,这也解释了为什么我在keepalive里写了好多组件,页面渲染的永远是第一个组件 - 拿到了第一个正在渲染的组件,取出他的组件名
name
,先判断用户是否传递了include
和exclude
,如果匹配上了,且不需要被缓存,就直接返回这个组件 - 如果可以被缓存,那么通过判断他的key是否存在,来生成一个唯一key
- 通过这个key来从缓存
cache
查找是否被缓存过 - 如果没有缓存过,把key和当前组件分别赋值给
vNodeToCache
,keyToCache
- 如果缓存过,那么将缓存中的这个key对应的组件信息赋值给当前渲染的组件,并且返回
问题解决
等等,我好像找到答案了,通过key去缓存的组件,在看问题代码
我那个朋友在router-view中传递了key,于是我让他打印了一下,发现每次路由切换,都会生成一个不同的viewkey
。我好像找到答案了,我让他去除了key,发现可以缓存了。ok了,问题解决了!
如何更新数据
问题是解决了,可是cacheVNode
只在mounted的时候调用,那他如何缓存多个呢?好奇的我发现,在updated
中也调用了。
好了,我悟了。原来如此,在每次render后,都会调用这个函数,从而实现缓存的功效
include和exclude的观察
问题又来了,如果include和exclude变化了,如何进行缓存呢?
于是我在mounted中看到了定义,对这两个参数进行watch监听,每次变换,重新执行pruneCache
这个函数
pruneCache做了什么
我又把ts改成js并且加了点注释,这个函数的功能就只有一个,通过传入的参数,去匹配组件名字是否满足缓存条件,如果不满足,那就去除这个缓存
const pruneCache = (keepAliveInstance, filter) => {
// 从keepalive组件中获取缓存的组件数据和keys列表
// 循环获取缓存的key 进行卸载
const { cache, keys, _vnode } = keepAliveInstance;
for (let key in cache) {
if (cache[key]) {
const name = cache[key].name;
// 使用name去匹配,不满足这个条件的就卸载
if (name && !filter(name)) {
console.log('卸载===>', name);
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
};
如何进行最大缓存约束
在这里,我又有个问题,如果设置了最大缓存数,内部如何判断哪些是需要缓存的,哪些是需要删除的?
其实这个问题在函数pruneCacheEntry
中有了答案,我这边简单加了点注释。该函数会清除缓存
const pruneCacheEntry = (cache, key, keys, current) => {
const entry = cache[key];
if (entry && (!current || current.tag !== entry.tag)) {
entry.componentInstance.$destroy();
}
// cache中的匹配的数据变为null
cache[key] = null;
// 删除数组中的数据
remove(keys, key);
};
这边还得依靠文章开头摘取的代码,默认认为第一项为过期,将它从缓存中去除
if (max && keys.length > parseInt(max)) {
// 卸载过期的缓存,这边默认是数组的第一项
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
如何保证经常使用的组件不会从缓存中去除
看到这里我总感觉怪怪的,如果我的组件第一个缓存,岂不是第一个从缓存中去除
答案不是的
if (cache[key]) {
// 缓存命中
// 将命中的缓存组件拿出来覆盖当前vNode的componentInstance
console.log(cache[key]);
vNode.componentInstance = cache[key].componentInstance;
// 由于需要将当前组件变成最新被使用
// 1.先删除命中的key
remove(keys, key);
// 2.重新追加到尾部,这样可以保证这个组件是最近被使用
keys.push(key);
}
这样可以保证最近使用的一次组件在末尾了。好家伙那些写框架的人都这么牛逼的吗
结语
通过本次问题,解决了朋友的问题,也让我对vue中keep-alive组件的实现有了更多的了解。感谢大家看到这里。如果觉得不错可以给小弟点个赞😀😀。如果有理解错误的地方,可以提出,我改!