这个内置组件相信大家都很熟悉,这里要说一个比较丢脸的事情,就是笔者曾经第一次找工作面试时,问道了keep-alive的原理是什么,讲真,我是一脸懵逼,甚至用法我都有点忘了,所以那次面试也黄了,因此从那以后我下定决心,一定要把vue所有的内置组件原理全部弄明白,所以才有了今天这篇博客的诞生。
为了更加友好的对待使用vue不久的小伙伴,我们先来简单地回顾一下keep-alive的用法吧!
1.回顾用法
keep-alive用法原文链接
keep-alive接受三个参数
include | exclude | max |
---|---|---|
匹配可以被缓存的组件 | 匹配不可以被缓存的组件 | 最大缓存组件数 |
白名单 | 黑名单 | 上限 |
小结:keep-alive组件可以帮助我们缓存包裹在其中的组件,只要在白名单中,且没有超出限制,都会被缓存,也就意味着今后,被缓存的组件是不会被销毁的,也就是说正常情况下,被缓存组建的mounted之前的生命周期钩子函数将不会再执行。因为没有组件并没有被销毁,因此再次渲染的时候省去了生成虚拟DOM的环节,从而达到了性能优化的目的。
我相信通过这么一列举,小伙伴们都应该能够看的懂每个参数代表的含义了叭,但是我们今天不是来学用法的,而是去了解keep-alive这个内置组件的原理的。所以接下来我们要上源码啦!
2.分析源码
温馨提示:源码的位置是在5221行到5370行。不过由于版本的问题可能不准确,大家在源码文件中搜索keepalive找到应该会更靠谱一些!
为了方便我们分析,来看一下源码到底做了什么?
var KeepAlive = {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode: function cacheVNode () {
var ref = this;
var cache = ref.cache;
var keys = ref.keys;
var vnodeToCache = ref.vnodeToCache;
var keyToCache = ref.keyToCache;
if (vnodeToCache) {
var tag = vnodeToCache.tag;
var componentInstance = vnodeToCache.componentInstance;
var componentOptions = vnodeToCache.componentOptions;
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag: tag,
componentInstance: 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: function created () {
this.cache = Object.create(null);
this.keys = [];
},
destroyed: function destroyed () {
for (var key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted: function mounted () {
var this$1 = this;
this.cacheVNode();
this.$watch('include', function (val) {
pruneCache(this$1, function (name) { return matches(val, name); });
});
this.$watch('exclude', function (val) {
pruneCache(this$1, function (name) { return !matches(val, name); });
});
},
updated: function updated () {
this.cacheVNode();
},
render: function render () {
var slot = this.$slots.default; // 这个就是需要缓存的组件
var vnode = getFirstComponentChild(slot); // 获取其虚拟DOM.
var componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode // 不允许缓存的条件 走到这里就返回了,keep-alive是不会做缓存处理的。
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
// 总之在这里找一个唯一的值最为key
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// 这个属于一个栈结构,让最访问最多了排在最前面,加大效率。
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本质上也是个组件,所以也和我们平时写组件一样需要书写配置项的,只是这个组件写的比较精妙,仅此而已。
好了,我们开始分析这个组件,接受3个参数,这个比较简单,我就不啰嗦了。
在created时,它给自己的data上新增了两个响应式变量
cache : { }
专门用来保存所缓存的组件
keys : [ ]
被缓存的所有组件的id所组成的数组
其次methods里面只有一个方法,其实就是往缓存对象里面放key , value的方法。但是在这里它加了一个判断,那就是只有当前实例对象有准备放入的vnode时,才进行放进盒子的操作。
看这个组件的时候要思考一个问题,一个vue实例组件,是先执行render函数呢,还是先执行mounted方法呢?思考一下?
答案是先执行render方法,因为mounted的意思是挂载完毕,意味着DOM已经生成了,那也就意味着肯定要执行了render函数DOM才能生成呀,所以我们先看这个组件的render函数,再分析mounted里面的内容。
其实里面的内容并不难,就是拿到被自己包裹的组件信息,包括名称,key,虚拟DOM等,如果不符合条件,直接返回虚拟DOM,也就意味着不会缓存,如果符合条件,就看一看当前缓存中有没有相应的key的组件,如果有,就直接返回缓存中的虚拟DOM
这个地方有个小技巧,就是它会尽可能的将最常访问的排在前面,这样可以减少遍历次数。
然后如果缓存中没有,就将当前的可缓存变量和可缓存key标位为可以。既赋值。告诉当前组件,我有需要缓存的组件。
然后render执行完之后,就到了mounted了,这里面首先会对exculde和inculde做一个监听,然后执行放入盒子的方法,然后因为之前已经告知组件我有需要缓存的内容,所以就缓存成功。
当下一次被包裹的内容变化,实际上意味着触发update方法,然后又回按照我们刚刚的流程走一遍,如此循环,这就是keepalive的原理。
3.重要概念
因此我们可以这样来理解keep-alive组件,本质上这其实还是一个组件,只不过这个组件并不渲染任何自己的内容,它渲染的都是被包裹的内容,并且内部维护了一个存放缓存的缓存器,被包裹的组件在符合条件的情况下,会被缓存到这个缓存器当中,只要第一次被缓存时过了,下一次就不需要再次生成,而是直接从缓存中读取。