function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } // 此处省略代码 ... var i; var data = vnode.data; if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode); } // 此处省略代码 ... }
由于我们的keep-alive是组件,所以在vnode创建的时候,会注入一些生命周期钩子,其中就包含prepatch钩子,其代码如下
prepatch: function prepatch (oldVnode, vnode) { var options = vnode.componentOptions; var child = vnode.componentInstance = oldVnode.componentInstance; updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ); }
由此可知,keep-alive组件的实例在此次根组件重渲染的过程中会复用,这也保证了keep-alive组件实例上面之前存储cache还是存在的
var child = vnode.componentInstance = oldVnode.componentInstance;
下面的updateChildComponent这个函数非常关键,这个函数担任了Foo组件切换到Bar组件的关键任务。我们知道,由于keep-alive组件是在此处是复用的,所以不会再触发initRender,所以vm.$slot不会再次更新。所以在updateChildComponent函数担起了slot更新的重任
function updateChildComponent ( vm, propsData, listeners, parentVnode, renderChildren ) { if (process.env.NODE_ENV !== 'production') { isUpdatingChildComponent = true; } // determine whether component has slot children // we need to do this before overwriting $options._renderChildren var hasChildren = !!( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots parentVnode.data.scopedSlots || // has new scoped slots vm.$scopedSlots !== emptyObject // has old scoped slots ); // ... // resolve slots + force update if has children if (hasChildren) { vm.$slots = resolveSlots(renderChildren, parentVnode.context); vm.$forceUpdate(); } if (process.env.NODE_ENV !== 'production') { isUpdatingChildComponent = false; } }
updateChildComponent函数主要更新了当前组件实例上的一些属性,这里包括props,listeners,slots。我们着重讲一下slots更新,这里通过resolveSlots获取到最新的包裹组件的vnode,也就是demo中的Bar组件,之后通过vm.$forceUpdate强制keep-alive组件进行重新渲染。(小提示:当我们的组件有插槽的时候,该组件的父组件re-render时会触发该组件实例$fourceUpdate,这里会有性能损耗,因为不管数据变动是否对slot有影响,都会触发强制更新,根据vueConf上尤大的介绍,此问题在3.0会被优化),例如
// Home.vue <template> <Artical> <Foo /> </Artical> </tempalte>
此例中当Home组件更新的时候,会触发Artical组件的强制刷新,而这种刷新是多余的。
继续,在更新了keep-alive实例的forceUpdate,之后再次进入到keep-alive的render函数中
render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) // ... }
此时render函数中获取到vnode就是Bar组件的vnode,接下去的流程和Foo渲染一样,只不过也是把Bar组件的vnode缓存到keep-alive实例的cache对象中。
当组件从Bar再次切换到Foo时
针对keep-alive组件逻辑还是和上面讲述的一样
执行prepatch
复用keep-alive组件实例
执行updateChildComponent,更新$slots
触发vm.$forceUpdate
触发keep-alive组件render函数
再次进入到render函数,这时候cache[key]就会匹配到Foo组件首次渲染时候缓存的vnode了,看下这部分逻辑
const key: ?string = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) } else { cache[key] = vnode keys.push(key) if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } }
由于keep-alive包裹的组件是Foo组件,根据规则,此时生成的key和第一此渲染Foo组件时生成的key是一样的,所以本次keep-alive的render函数进入到了第一个if分支,也就是匹配到了cache[key],把缓存的componentInstance赋值给当前vnode,然后更新keys(当存在max的时候,能够保证被删除的是比较老的缓存)。
很多同学可能会问,这里设置vnode.componentInstance会有什么作用。这里涉及到vue的源码部分。