在分析之前,先将例子进行小小的改动,改动后的文件如下
main.js文件
app.vue文件
在components文件下新增child.vue,并在app.vue中引入
通过之前几篇的分析我们知道,import vue执行初始化构建了vue类,new vue调用init方法判断el存在执行mount,在mount过程中调用mountComponent并在该函数中定义渲染watcher,而在渲染watcher内又调用了updateComponent函数,该函数首先拿到vue的渲染vnode,后调用update执行patch,patch的过程其实就是递归调用原生dom方法去create和insert,而在createElement过程中将根据类型生成不同的vnode,也就是在new vue时指定的'el:#app'调用的是create-element,在子组件app patch时调用的create-component。通过上一节(組件的vnode)分析我们拿到了组件的Vnode,它将作为_render函数的返回值,也就是_update的入参,因此代码定位到src\core\instance\lifecycle.js下的
进入update,该函数的入参为组件Vnode和false,变量缓存的值如下
a-vm即Vue,因为this在es5中指向的是其调用者
b-prevEl:如果当前组件是子组件的话,那么其指向的父组件所对应的dom,因为在存在父组件的时候,父组件的patch过程的返回值将被vm.$el缓存,而vm.$el同时又作为patch的参数传入,也就是说,在patch之前vm.$el是有值的,换句话说,在update之前是有值的。因此我们向前查找mount过程,在mountComponent中向vm.$el挂载了值
而el则是在调用时传入的,因此继续向上查找,找到$mount
这里的el又是从上一级传入的,因此继续向前查找到init方法
我们发现,在调用mount方法时传入了el参数,而在app.vue中并未指定,因此为undefined,即prevEl=undefined
c-prevVnode取得是vm._vnode,而vm._vnode在后边保存的是vnode,因此我们需要找到new vue过程中的vnode是什么,我们发现在组件init的过程中调用了
而在initRender中又执行了
因此prevVnode=null
d-restoreActiveInstance调用setActiveInstance
该函数首先保存了上一次的instance,而上一次是vue,接着又向activeInstance缓存了一份当前组件的vm,因此,prevActiveInstance 和 activeInstance实际上是父子关系,因此prevActiveInstance=vue,activeInstance=vm,并返回一个函数
e-vm._vnode=当前的组件Vnode
代码向下
由于prevVnode=null,因此走进if判断,执行patch方法,该方法入参为
(undefined,'组件Vnode',false,false)
查找patch方法,该方法在src\platforms\web\runtime\index.js文件中被挂载到vue原型,根据import引入路径查找到patch
a-isUndef(vnode)=false,跳过
b-isUndef(oldVnode),由于oldVnode是undefined,故为true,进入if判断,调用createElm,该方法入参为
(‘组件vnode’,[])
a-vnode.elm=undefined,跳过
b-调用createComponent方法,该方法入参为
('组件vnode',[],undefined,undefined)
从代码可以看出,该函数会返回两个值,一个是true,一个是undefined。从注释可以看出,返回true的前提是执行了i,也就是i=i.hook,因此核心是vnode.data是什么
我们在上一节(组件的vnode化)分析中得知,在构建vue的vnode过程中,对app.vue进行构建并生成了一个组件对象,该组件对象上挂载了data.hook
组件的vnode如下
因此i的值如下
a-isDef(i)值为true,进入判断
b-i = i.hook为true,i.hook.init为true,进入判断执行init方法;该方法在组件vnode化的过程中,在createComponent中调用installComponentHooks被添加到组件的hook上,因此调用的实际上是
该方法的入参为
('组件vnode',false)
a-vnode.data.keepAlive=false,代码走向else,调用createComponentInstanceForVnode方法
该方法入参为
('组件vnode',vm)
a-inlineTemplate为false,跳过
b-new vnode.componentOptions.Ctor:打开vnode类定义
componentOptions是在生成vnode时传入的参数,对应的参数即在生成vnode时传递的如下
而Ctor则是调用vue.extend的返回值,我们在上一节分析过,它实际上是定义了一个Sub构造函数,并通过原型链继承了vue的方法,因此new vnode.componentOptions.Ctor调用的实际是
这里的this指向的是vue,因此调用的实际上是vue.init方法
该方法的入参为new vnode.componentOptions.Ctor时传递的object
代码向下
a-vm指向vue
b-vm._uid;在new vue的时候也调用了init方法,因此这里至少一级递增一次,由于++在后是先使用后递增,故为1
c-options && options._isComponent=true,进入if判断,调用initInternalComponent方法
该方法的入参为
(vue,'new vnode.componentOptions.Ctor时传递的object')
i-opts;Object.create方法将创建一个新对象,并将该对象的__proto__指向参数对象
ii-parentVnode;取自options._parentVnode,该键保存着vnode的引用
iii-opts.parent是vue;当我们调用data.hook.init时传递的是app.vue的组件vnode,在该init方法下调用了createComponentInstanceForVnode,传递了当前的组件vnode和activeInstance,并将activeInstance作为parent合并到options上
因此,查找activeInstance的值,该值在update过程中调用setActiveInstance设置为vue,是一个全局的值
iv-opts._parentVnode='组件vnode'
回到组件app.vue过程的init方法,调用initLifecycle
a-parent && !options.abstract=true;进入判断,向options挂载$children,成员为当前组件vnode
b-vm.$parent=vue;故组件的$parent指向vue,vue的$children指向vnode,两者是父子关系
回到组件的init过程,el不存在,init方法结束,回到组件的hook的init方法中,调用child.$mount方法
拿到的el为undefined,调用mountComponent,在mountComponent方法中再一次实例化Watcher,在Watcher中又再一次update和render,此时的render构建的是app.vue内部的元素,拿到的vm就是app.vue的组件对象,该对象中使用parent指向vue,$options中存在parent指向vue,_parentVnode代表在vue组件对象中的占位符,然后render之后再一次update走patch过程,由于app.vue中存在组件child,因此又会生成子组件的vnode,并调用子组件的init-mount-render-update,依次类推。当最后一次嵌套的组件为普通节点时,将执行挂载。因此定位代码至createElm函数中
createComponent返回undefined,表示不存在更多的子组件,代码向下,调用createChildren递归调用createElm一个一个生成dom节点,最后执行insert插入到dom文档当中