能看这篇文章的我相信都用过keep-alive,keep-alive是vue的内置组件,主要功能是会缓存组件。但是缓存的只是第一个组件,如果在keep-alive内部放置多个组件是不会被缓存的。我们从源码来具体分析keep-alive的工作原理吧。
<template>
<div>
<keep-alive>
<aa v-if="a%2"></aa>
<bb v-else></bb>
</keep-alive>
<div @click="add">add</div>
</div>
</template>
<script>
import aa from './aa.vue'
import bb from './bb.vue'
export default {
name: "app",
components: {
aa,
bb
},
data() {
return {
a: 1
}
},
methods: {
add() {
this.a++
}
}
};
</script>
...
// aa.vue
<template>
<div>
我是aa
</div>
</template>
<script>
export default {
name: 'aa'
}
</script>
...
// bb.vue
<template>
<div>
我是bb
</div>
</template>
<script>
export default {
name: 'bb'
}
</script>
首先会执行app.vue的render方法生成vnode:
然后会调用该组件的data.hook.init(只有组件才有这个方法)方法生成app组件的vm实例:
init: function (vnode, hydrating) {
if (vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
}
else {
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
}
这里的child就是app.vue的vm实例,接下来执行vm实例上的$mount方法挂载组件。挂载的时候会执行组件的render方法。我们可以看到此时a为1所以会渲染aa组件。执行的顺序是从里到外,从前到后,从左到右。所以先执行_vm.a,在执行_c(“a”)生成vnode,然后执行_c(“keep-alive”)生成vnode,再依次往下执行_vm._v(" ")…,执行完后执行_c(“div”,[…])生成最终的vnode。
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
...
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c(
"div",
[
_c("keep-alive", [_vm.a % 2 ? _c("aa") : _c("bb")], 1),
_vm._v(" "),
_c("div", { on: { click: _vm.add } }, [_vm._v("add")]),
],
1
)
}
生成如下:
接下来执行app组件的_update方法,后执行createElm方法。由于第一个vnode是keep-alive,所以会生成keep-alive的vm实例:
可以看到aa组件被放入$slot当中。此时会继续去挂载keep-alive组件,然后去执行该组件的渲染方法:
render: function () {
var slot = this.$slots.default;
var vnode = getFirstComponentChild(slot);
var componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
var name_2 = _getComponentName(componentOptions);
var _a = this, include = _a.include, exclude = _a.exclude;
if (
// not included
(include && (!name_2 || !matches(include, name_2))) ||
// excluded
(exclude && name_2 && matches(exclude, name_2))) {
return vnode;
}
var _b = this, cache = _b.cache, keys = _b.keys;
var key = vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? "::".concat(componentOptions.tag) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove$2(keys, key);
keys.push(key);
}
else {
// delay setting the cache until update
this.vnodeToCache = vnode;
this.keyToCache = key;
}
// @ts-expect-error can vnode.data can be undefined
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
}
可以看出会获取插槽里面的第一个vnode就是aa.vue,并且会判断有没有缓存,有的话会移除旧的位置并放入首位(LRU算法)。否则会设置为待缓存vnodeToCache。可以看出keep-alive的工作原理其实就是返回首位的vnode,放入超过一个的组件不会被显示。最后返回的vnode:
此时执行keep-alive组件的_update方法去把当前的vnode生成组件实例并挂载,挂载的最后结果就是将aa的template下的vnode转换成真实的dom。我们再看看什么时候去增加缓存的:
methods: {
cacheVNode: function () {
var _a = this, cache = _a.cache, keys = _a.keys, vnodeToCache = _a.vnodeToCache, keyToCache = _a.keyToCache;
if (vnodeToCache) {
var tag = vnodeToCache.tag, componentInstance = vnodeToCache.componentInstance, 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;
}
}
}
在组件被挂载之前会调用cacheVNode方法,可以看出取出了vnodeToCache的值,缓存了组件vm,并放入keys里面然后清空了vnodeToCache。当有缓存的时候会执行:
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove$2(keys, key);
keys.push(key);
}
从cache[key]中取出vm实例然后直接给当前的vnode,相当于省去了实例化的过程,然后放入首位。
总结
keep-alive组件就是取出内部第一个组件并返回,然后在挂载前放入缓存列表。当切换的时候如果有缓存的时候直接拿当前的实例覆盖新的vnode的实例,而不用重新生成vnode。