下面带着生成的vnode进入_update函数:
接着_update方法的作用是把VNode渲染成真实的DOM,他定义在src/core/instance/lifecycle.js中。_update的核心就是调用vm.__patch__方法,首次渲染时,vm.vnode为空,所以给__patch__传入的vm.$el为真实的#app DOM节点:
这个方法不同的平台是不一样的,在web平台中它的定义在src/platforms/web/runtime/patch.js文件中,通过createPatchFunction来生成,这里调用createPatchFunction的方法是从core/vdom/patch中引入的,但是此函数的参数是从web平台的文件夹引入的,nodeOps封装了一系列DOM操作的方法,modules定义了一些模块的钩子函数的实现,通过这种方式把核心代码和平台代码进行解耦:
patch函数中主要调用的函数是createElm函数,注意此处的parentElm是body这个DOM,在最后把vue实例化完DOM节点的时候会挂载上去:
在createElm中:
判断vnode是否包含tag,如果包含,先简单对tag的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台DOM的操作去创建一个占位符元素vnode.elm。
调用createChildren方法去创建子元素。遍历子虚拟节点,递归调用createElm,注意在遍历过程中会把vnode.elm作为父容器的DOM节点占位符传入。
调用invokeCreateHooks方法执行所有的create的钩子并把vnode push到insertedVnodeQueue中。
最后调用insert方法把DOM插入到父节点中,因为是递归调用,子元素会优先调用insert,所以整个vnode树节点的插入顺序是先子后父。
最后如果不是标签节点,那么就是注释节点或者文本节点这两种叶子节点,直接生成vnode节点,插入到父DOM元素上。关于节点种类请参考《结合源码理解vue列表渲染v-for中的key属性》
上面第4条说是递归调用,其实就是在createChildren函数中,还是会迭代对children调用createElm函数:
在对button元素执行createElm时,会通过nodeOps.createElement API来新建DOM节点,插入到父节点parentElm中:
但是作为组件的keep-alive却会走到createComponent中,并返回true:
在createComponent中,先来执行组件在生成vnode时componentVNodeHooks混入的init钩子:
在init钩子中,由于vnode.componentInstance和vnode.data.keepAlive未被定义,所以执行到createComponentInstanceForVnode函数,此处vnode为keepalive组件的vnode:
activeInstance为当前的vue实例,通过$options可以看出为根节点挂载的vue实例:
然后进入到createComponentInstanceForVnode函数中,通过之前继承的构造函数来实例化组件实例然后返回,注意实例化时传入了自定义的options:
在new语句中,会走到之前组件继承vue初始化函数的这个地方,执行this._init初始化组件实例,而此时的this为VueComponent实例:
在继承的_init方法中,得到_uid为1,传入的options._isComponent为true,所以进入initInternalComponent函数:
在initInternalComponent函数中以vm.constructor.options为原型实例化出的对象赋给vm.$options,就是源码中的keep-alive对象,其中自己定义了render方法:
并在vm.$options中保留传入options中的parent和_parentVnode:
回到Vue初始化方法原型上的_init,对组件实例各种初始化 ,最后vm.$options.el中并未定义,没有执行_init中的vm.$mount方法,就退出了。
回到init钩子中,最后由child调用$mount来实现挂载: