render () { const slot = this.$slots.default // 获取第一个组件子节点 const vnode: VNode = getFirstComponentChild(slot) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) const { include, exclude } = this if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this const key: ?string = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key // 1、如果缓存中存在该vnode,从缓存中取得该组件的实例(一个组件对应一颗vnode树,同时一个组件对应一个vue子类的实例),不再重新创建 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest // 将当前的组件的key作为最新的缓存(更新其在keys数组中的顺序) remove(keys, key) keys.push(key) } else { // 2、如果未命中缓存,添加到缓存 cache[key] = vnode keys.push(key) // 如果缓存超过限制,淘汰最旧的缓存 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } // 标记为keepAlive组件 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) }
keep-alive内部就是单独提供了render函数来自定义了vnode的创建逻辑。首先keep-alive获取到其所包裹的子组件的根vnode,然后去cache中查找该组件是否存在。
如果cache
中不存在子组件vnode,则以{子组件名: 子组件vnode}
的形式保存到cache
对象中。同时将子组件名字保存到keys
数组中。同时如果当前缓存的数量已经超过max
所设置的最大值,需要淘汰掉最近最少使用的缓存项(LRU)。
如果cache
中存在子组件vnode,那么只需要复用缓存的组件vnode的组件实例(componentInstance)。同时需要将该子组件vnode在缓存中顺序调到最前面,这个主要是为了在缓存不足时,正确地淘汰缓存项。
举例说明
最后通过一个例子加深一下理解。
<div id="app"> <keep-alive><component :is="view"></component></keep-alive> <button @click="view = view =='count'? 'any': 'count'">切换组件</button> </div>
Vue.component("count", { data() { return { count:0 }; }, template: "<div @click='count+=1'>点了我 {{count}} 次</div>" }); Vue.component("any", { template: "<div>any</div>" }); new Vue({ el: "#app", data: { view: "count" } });
由于view
默认值是count
,因此keep-alive包裹的子组件是count
。此时keep-alive的缓存中为空,因此会把组件count
的vnode添加到缓存。缓存结果为:
cache = {1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}}, componentOptions, componentInstance, ...} keys = ["1::count"]
页面显示结果为:
点击一下组件count
,组件的显示内容变成"点了我1次",然后切换到组件any
。与count
组件相同,由于在keep-alive的缓存中还未保存any
组件的vnode,因此需要将any
添加到缓存中。此时缓存结果变成了:
cache = { 1::count: {tag: "vue-component-1-count", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...}, 2::any: {tag: "vue-component-2-any", data:{tag: "component", hook: {…}}, componentOptions, componentInstance, ...}, } keys = ["1::count", "2::any"]
页面显示结果为:
再次点击切换组件,切回count
。此时count
组件的vnode在缓存中已经存在,所以直接复用了原来count
组件vnode中所保存的组件实例,组件实例中保存了原来的count
值,因此组件切换完后,组件的状态也跟着还原了回来。
下图为count
组件实例的状态,可以看到count
的值得到了保持:
最终页面显示结果为:
从上面的分析可知,如果组件被包裹在keep-alive组件中,组件vnode会缓存到cache
中。而组件的状态又会保存在组件实例中(componentInstance),当组件再次切换回来时,keep-alive直接将之前缓存的状态进行还原,也就实现了组件状态的保持。