在上一篇中我们介绍了根组件root的初始化渲染过程:
| 1 | new Vue({ | 
当时我们提到,root的_vnode的componentInstance中存储了App组件的实例(后面称为app):
| 1 | const child = vnode.componentInstance = createComponentInstanceForVnode( | 
在app这个实例mount之后,我们把app.$el插入到页面上整个root就渲染出来了,其中的数据结构如下:
 
                本篇中我们详细看看app的创建及首次渲染过程,我们从createComponentInstanceForVnode开始,在本例中,我们传给你它的2个参数依次为root._vnode和root:
| 1 | export function createComponentInstanceForVnode ( | 
App的构造函数是Vue.extend中定义的,最终会调用Vue.prototype._init进行初始化:
| 1 | // src/core/instance/init.js | 
这个初始化方法主要有三个步骤:
- 得到vm的最终的$options - 主要是把参数中的options和构造函数的options合并起来作为最终的 - $options,这里我们options._isComponent是true,所以走的是initInternalComponent,这个细节不讨论,有兴趣大家可以看看参考文档(http://hcysun.me/vue-design/art/)
- 做各种初始化 - 有兴趣参考(http://hcysun.me/vue-design/art/),这里我们只列出一点:在initLifecycle中我们设置了`vm.$parent`及`vm.$root`: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- // src/core/instance/lifecycle.js 
 export function initLifecycle (vm: Component) {
 const options = vm.$options
 let parent = options.parent
 if (parent && !options.abstract) {
 ...
 parent.$children.push(vm)
 }
 vm.$parent = parent
 vm.$root = parent ? parent.$root : vm
 }- 我们这里的options里面是传了parent选项的,这里的值是root 
- mount - 这里if条件不成立,所以没有mount,mount发生在实例被创建之后 
app创建之后,我们马上进行了mount:
| 1 | child.$mount(hydrating ? vnode.elm : undefined, hydrating) | 
第一个参数是undefined,第二个参数是false。我们知道,mount会重新走_render及_update的过程,我们先看看_render,简化如下:
| 1 | // src/core/instance/render.js | 
在render之前我们设置了实例的$vnode属性,render之后又设置了vnode.parent属性,_render函数返回的vnode传入_update,我们看看update:
| 1 | // src/core/instance/lifecycle.js | 
prevEl和prevVnode分别保存之前的根元素($el)和_vnode,在首次渲染之前,它们都是空的。然后设置新的_vnode,并在patch之后设置新的$el,并且在patch之前修改全局变量activeInstance,设置为自己,这里即app实例,patch完之后再还原成root。最后是一个if语句,在本例中是满足的,所以把root的根元素置为和app的一样,所以经过_render和_update时候数据结构变成:
 
                理解了大概的数据结构之后,我们再详细看看app的render和update过程
render
App组件的html模板为:
| 1 | <template> | 
它生产的render函数如下:
| 1 | // webpack:///./src/App.vue?5b21 | 
我们在_render中是这么调用的:
| 1 | vnode = render.call(vm._renderProxy, vm.$createElement) | 
vm._renderProxy在生产环境下就是vm,_vm._m其实就是staticRenderFns,_c定义如下:
| 1 | vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) | 
最终执行的是下面这段代码:
| 1 | return _c("div", { attrs: { id: "app" } }, [ | 
最终执行的是_createElement:
| 1 | // src/core/vdom/create-element.js | 
对顶层的div来说,传递给这个函数的参数为:
- context:当前实例,即app 
- tag:”div” 
- data: - { attrs: { id: "app" } }
- children: - 1 
 2
 3
 4
 5
 6- [ 
 _c("img", {
 attrs: { alt: "Vue logo", src: require("./assets/logo.png") }
 }),
 _c("div", [_vm._v("hello, world!")])
 ]- 其实就是包含2个vnode元素的数组 
- normalizationType:undefined 
根据这些参数,这个函数的执行路径为:
| 1 | // src/core/vdom/create-element.js | 
直接是以内置div标签new一个vnode出来
update
得到这个vnode之后,我们调用patch:
| 1 | vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) | 
这是vm.$el是undefined,因为这是首次渲染,我看把视角转向patch函数:
| 1 | // src/core/vdom/patch.js | 
这次很简单,因为oldVnode是空的,所以直接调用createElm基本就结束了,这次只传了2个参数,我们看看这个函数这次的执行路径:
| 1 | // src/core/vdom/patch.js | 
这次createComponent是不会返回true的,且调用之后没有任何效果,因为这里的vnode只是一个div的包装而已。代码直接进入第一个if执行下面这行代码:
| 1 | vnode.elm = nodeOps.createElement(tag, vnode) | 
它直接创建一个div元素!因为children还有2个vnode,所以再调用createChildren创建子元素,这样app的整个dom就创建好了,然后patch直接返回根元素。最后回到_update把这个元素赋给vm.$el,整个数据结构变成:
 
                在上一篇我们知道,这个根元素最终会被root插入到文档中从而展示在页面上。至此,下面这两三行简单的代码基本分析完了:
| 1 | import App from './App.vue' | 
可以看到,这后面其实牵涉到很多逻辑,何况我们还省略了很多细节,但是更复杂的情况也能从这里推导出来。