keep-alive组件是Vue的内部组件,Vue官方对于keep-alive的描述是:
keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。当组件在keep-alive内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行
从上面的描述可以得到如下几点信息:
keep-alive是抽象组件,其特点有:
keep-alive是vue内部组件,具体组件定义为:
var KeepAlive = {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
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.$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); });
});
},
render: function render () {
// 相关代码
}
};
Vue组件创建虚拟节点VNode对象是通过render函数来实现,相关逻辑:
vm._update(vm._render(), hydrating);
vm的_render函数就是执行render函数返回对应的vnode,而keep-alive组件不会渲染为一个DOM元素,核心的思想在于render函数最后返回的vnode对象。
render() {
// 核心代码
var slot = this.$slots.default;
var vnode = getFirstComponentChild(slot);
// 其他逻辑
return vnode || (slot && slot[0])
}
上面代码是内部组件keep-alive其render函数的一些逻辑,从上面代码逻辑中可以知道keep-alive返回虚拟节点是其子组件vnode或子标签vnode。
这部分逻辑是在Vue初始化过程中initLifecycle函数的中,核心代码如下:
var parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
可以看出组件options.abstract是false才会将组件实例对象放置到$children中,这是为什么keep-alive组件本身不会出现在组件的父组件链中的原因。
实际上这个特点结合keep-alive组件自身的render函数相关逻辑来看,具体逻辑如下:
render() {
var vnode = getFirstComponentChild(slot);
var componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// 相关缓存逻辑
}
}
这里缓存逻辑的关键在于componentOptions是否存在,而componentOptions表示是组件的配置文件对象,而Vue中创建VNode对象传入该属性的处理只有一处即根节点render函数的执行处理createComponent中调用new VNode传入。
根节点render函数处理只有组件才会调用createComponent来处理,所以对于原生HTML标签或SVG标签是不会存在缓存处理的
这里需要说明的是keep-alive针对动态组件缓存,这里的动态组件严格来说就是组件而已,而keep-alive针对动态组件缓存这个说法更准确来说是:
在每次渲染过程中,keep-alive会缓存其包括的第一个需要显示出来的组件
渲染出来的组件只会是keep-alive组件的第一个,如果存在多个需要显示的组件也只会显示第一个。例如:
<keep-alive>
<component1>测试1component1>
<component2>测试2component2>
keep-alive>
上面的代码只会显示出现测试1内容,而测试2内容是不会显示出现的,这都是源于keepa-alive中getFirstComponentChild的逻辑处理。
实际上还是需要结合keep-alive的render方法逻辑来看,实际逻辑是清晰的,具体逻辑如下:
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// 移除对应key,更新当前key的新鲜度
remove(keys, key);
keys.push(key);
} else {
cache[key] = vnode;
keys.push(key);
// 当超过缓存上限时,移除最老的key
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
实际上keep-alive缓存的实现就是通过cache对象 + keys标识新鲜度来实现的,逻辑还是很清晰的。
从Vue整体构建过程中可知(可通过Vue内部组件 这篇文章了解)Vue中组件对应的虚拟节点都存在一系列的hook钩子函数,其中insert和destroy是该部分内容需要关注的点。
Vue pacth阶段会调用patch函数来处理相关逻辑,其中必然会执行invokeInsertHook逻辑,即调用虚拟节点的insert钩子函数,该钩子函数重要逻辑如下:
insert: function insert(vnode) {
// 其他逻辑
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true /* direct */);
}
}
}
通过上面逻辑可知对于keepAlive组件会调用相关activateChildComponent(这里先暂时不管queueActivatedComponent的逻辑),而activateChildComponent函数主要是两个逻辑:
同activated生命周期触发,在Vue patch阶段会对旧节点执行invokeDestroyHook操作,即调用虚拟节点的destroy钩子函数,该钩子函数重要逻辑如下:
destroy: function destroy (vnode) {
var componentInstance = vnode.componentInstance;
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
deactivateChildComponent函数的逻辑跟activateChildComponent函数类似,主要是两个逻辑:
实际上这2个组件同用,无非就是keep-alive包裹transition或者transition包裹keep-alive这2种情况。这里分析keep-alive包裹transition这种情况,通过这个情况分析来分析最优效果。实际上keep-alive包裹transition这种情况动画效果是生效的,而不是说不没法工作的。
通过上面对keep-alive的逻辑分析,可知keep-alive总会获取第一个存在满足条件的组件,而这个条件简单来说就是存在componentOptions值(而实际上普通自定义组件都存在该值)。而keep-alive这个逻辑会导致缓存transition这个组件本身,而对于transition组件包括的子组件是不起效果的。
当需要用到keep-alive + transition肯定是希望尽可能多缓存组件,而keep-alive包裹transition这种情况没法缓存实际参入动画的组件,仅仅是缓存了transition组件本身
从源码来看,实际应用场景还是通过transition包裹keep-alive来处理动画组件的缓存,实际上Vue官网有一处例子:
<transition>
<keep-alive>
<component :is="view">component>
keep-alive>
transition>
之后会开另一篇文章聊聊transition,从源码分析该内部组件的具体逻辑。