let Foo = { template: '<div>foo component</div>', name: 'Foo' } let Bar = { template: '<div>bar component</div>', name: 'Bar' } let gvm = new Vue({ el: '#app', template: ` <div> <keep-alive> <component :is="renderCom"></component> </keep-alive> <button @click="change">切换组件</button> </div> `, components: { Foo, Bar }, data: { renderCom: 'Foo' }, methods: { change () { this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo' } } })
上面例子中,根实例的template会被编译成如下render函数
function anonymous( ) { with(this){return _c('div',{attrs:{"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})],1),_c('button',{on:{"click":change}})],1)} }
可使用在线编译:
根据上面的render函数可以知道,vnode生成的过程是深度递归的,先创建子元素的vnode再创建父元素的vnode。
所以首次渲染的时候,在生成keep-alive组件vnode的时候,Foo组件的vnode已经生成好了,并且作为keep-alive组件vnode构造函数(_c)的参数传入。
_c('keep-alive',[_c(renderCom,{tag:"component"})
生成的keep-alive组件的vnode如下
{ tag: 'vue-component-2-keep-alive', ... children: undefined, componentInstance: undefined, componentOptions: { Ctor: f VueComponent(options), children: [Vnode], listeners: undefined, propsData: {}, tag: 'keep-alive' }, context: Vue {...}, // 调用 $createElement/_c的组件实例, 此处是根组件实例对象 data: { hook: { init: f, prepatch: f, insert: f, destroy: f } } }
此处需要注意组件的vnode是没有children的,而是将原本的children作为vnode的componentOptions的children属性,componentOptions在组件实例化的时候会被用到,同时在初始化的时候componentOptions.children最终会赋值给vm.$slots,源码部分如下
// createComponent函数 function createComponent (Ctor, data, context, children, tag) { // 此处省略部分代码 ... var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), data, undefined, undefined, undefined, context, { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory ); return vnode }
Vue最后都会通过patch函数进行渲染,将vnode转换成真实的dom,对于组件则会通过createComponent进行渲染
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { var i = vnode.data; if (isDef(i)) { var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true } } }
接下去分两步介绍
keep-alive组件本身的渲染
keep-alive包裹组件的渲染,本例中的Foo组件和Bar组件
先讲讲本例中针对keep-alive组件本身的渲染
根组件实例化
根组件$mount
根组件调用mountComponent
根组件生成renderWatcher
根组件调用updateComponent
根组件调用vm.render()生成根组件vnode
根组件调用vm.update(vnode)
根组件调用vm.patch(oldVnode, vnode)
根组件调用createElm(vnode)
在children渲染的时候,如果遇到组件类型的vnode则调用createComponent(vnode),而正是在这个过程中,进行了子组件的实例化及挂载($mount)