案例
<!DOCTYPE html>
<body>
<div id="app"></div>
</body>
<script src="vue.js"></script>
<script>
let ButtonCounter = {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
beforeCreate() {
console.log('child beforeCreate')
},
created() {
console.log('child created')
},
beforeMount() {
console.log('child beforeMount')
},
mounted() {
console.log('child mounted')
},
beforeUpdate() {
console.log('child beforeUpdate')
},
updated() {
console.log('child updated')
},
beforeDestroy() {
console.log('child beforeDestroy')
},
destroyed() {
console.log('child destroyed')
},
}
let childComp = {
template: '<div>{{msg}}<button-counter/></div>',
components: {
'button-counter': ButtonCounter
},
data() {
return {
msg: 'QAAA'
}
},
beforeCreate() {
console.log('parent beforeCreate')
},
created() {
console.log('parent created')
},
beforeMount() {
console.log('parent beforeMount')
},
mounted() {
console.log('parent mounted')
},
beforeUpdate() {
console.log('parent beforeUpdate')
},
updated() {
console.log('parent updated')
},
beforeDestroy() {
console.log('parent beforeDestroy')
},
destroyed() {
console.log('parent destroyed')
},
}
let app = new Vue({
el: '#app',
render: h => h(childComp),
beforeCreate() {
console.log('init beforeCreate')
},
created() {
console.log('init created')
},
beforeMount() {
console.log('init beforeMount')
},
mounted() {
console.log('init mounted')
},
beforeUpdate() {
console.log('init beforeUpdate')
},
updated() {
console.log('init updated')
},
beforeDestroy() {
console.log('init beforeDestroy')
},
destroyed() {
console.log('init destroyed')
},
})
</script>
init beforeCreate
init created
init beforeMount
parent beforeCreate
parent created
parent beforeMount
child beforeCreate
child created
child beforeMount
child mounted
parent mounted
init mounted
分析
初始化 Vue
new Vue()
- 通过外部
new Vue()创建vue实例,进入_init()方法中
通过 new 创建实例会进行以下四步:
- 创建一个新对象
- 将 this 指针指向这个新对象
- 执行构造函数中的代码
- 返回新对象
- 所以在
_init方法的第一步,我们就将vm = this,此时的this就是指向vue实例的。 - 接着在
vm上添加了很多属性。 - 此时传入的
options没有_isComponent属性,所以使用mergeOptions进行配置合并,并赋值给vm.$options属性。 - 然后初始化生命周期,事件总线和
render方法,接着调用callHook执行beforeCreated这个生命周期的事件。
init beforeCreate
我们浅看一下 callHook 中的操作:
var prev = currentInstance // 开始时 currentInstance 为 null
setCurrentInstance(vm) // 设置 currentInstance 为 当前的 vue 实例
// ... 执行 vm.$options[hook]
setCurrentInstance(prev) // 重置 currentInstance 为 null
- 再通过
initInjections,initProvide,还有initState初始化data,props,methods等数据,在这之后,调用callHook执行created这个生命周期的事件。
init created
- 由于传入的
options中有el: '#app'属性,所以进入vm.$mount(vm.$options.el)执行挂载的过程。一般只有new Vue()的时候会传入el,组件初始化的时候不会走到这。
vm.$mounted
- 根据
el找到html中的真实Dom。
<div id="app"></div>
一旦写了 render,这里写的 el,将没有任何意义,在最终生成的 dom 中不会存在。
- 在
$mount开始的时候,有render用手写的render,如h => h(childComp),没有render则将el或者template属性转换成render, 如_c('button-counter')或_c('div')。而此案例中传入的本来就是render:
h => h(childComp)
- 从而直接进入
mountComponent方法,设置vm.$el为真实的Dom节点<div id="app"></div>,紧接着调用callHook执行beforeMount这个生命周期的事件。
init beforeMount
- 后面创建了一个监听,先不用看这个
watcher,我们只需要看到updateComponent方法,在初始化渲染的时候就执行了这个方法,vm._update(vm._render(), undefined)中使用_render方法将render函数转换为vnode,在_update方法中将vnode转换为真实dom。 - 当执行完
updateComponent方法之后,会判断vm.$vnode是否为null,如果为空会设置vm._isMounted = true,并执行mounted生命周期。这块内容我们稍后再看,先进入updateComponent方法看看。
vm._render()
-
在
_render方法中,将vm.$vnode赋值为vm.$options._parentVnode,当前实例的父虚拟节点当然为undefined。 -
设置
currentInstance=vm,设置成了当前的vue实例。 -
然后执行
render,生成vnode:
vnode = render.call(vm._renderProxy, vm.$createElement);
render 方法为 h=>h(childComp),使用 call 方法调用,即 h 为 vm.$createElement 方法。
4. 即执行 vm.$createElement(childComp),也就是传入的 tag 参数为 childComp 对象,由于是手写的 render,所以 alwaysNormalize 为 true,随后调用 _createElement 方法。
- 如果传入的
tag是对象,则直接执行createComponent方法,将传入的tag对象继续往下传,如h => h(childComp)- 如果传入的
tag是字符串,且是保留的标签,那么直接通过new Vnode创建出对应的vnode,如_c('div')。- 如果传入的
tag是字符串,但是不是保留标签,那么会在组件的components属性中查找,找到对应标签名的组件对象再传入createComponent执行,如_c('button-counter')。
在 _createElement 方法中由于 children 为 undefined,所以不需要看 normalizeChildren 方法。由于传入的 tag 为 childComp 对象,会进入 createComponent 方法。
总结一下,当手写 render 的时候,传入的如果是对象或者未知标签,那么就会被当作组件处理。当是由 template 生成的 render,那么在 template 中的未知标签的元素会被当作组件处理。
- 执行
createComponent生成组件的vnode。
vnode = createComponent(tag, data, context, children);
function createComponent(Ctor, data, context, children, tag){...}
createComponent 接收了一个参数 Ctor,就是我们传入的 tag 对象,如 childComp 对象,
- 首先我们将这个
Ctor对象进行包装,将其设置为一个VueComponent方法对象,继承自Vue对象,并将传入的childComp对象merge到VueComponent对象的extendOptions和options属性上。 - 然后使用
installComponentHooks生成包含hook还有on属性的data对象,hook中包含init、prepatch、insert、destroy四个方法。 - 最后通过
Ctor和data生成的vnode,其tag为vue-component-1,组件的vnode没有children。
-
生成的
childComp组件的vnode后,返回_render方法中,将组件的vnode的parent属性设置为_parentVnode,当前为null。然后将_render方法生成的vnode,传入_update方法生成真实的Dom。 -
最终生成的
vnode为:
{
tag: "vue-component-1",
data: { on: undefined, hook: { init: f, prepatch: f, insert: f, destroy: f,} },
children: undefined, // 组件的 vnode 没有 children
parent: undefined,
componentOptions: {
// Ctor 是一个 VueComponent 方法
Ctor: {
cid: 1,
options: {}, // childComp 对象的属性都 merge 到了上面
...
}
},
...
}
vm._update(vnode)
- 在
_update方法中,将preEl = vm.$el(<div id="app"></div>),prevVnode = vm._vnode,当前的vm._vnode为null。 - 将当前
vm(Vue) 设置为了全局变量activeInstance,且将vm保存在了局部作用域中的prevActiveInstance变量中,在执行完__patch__方法后会将保存的vm重新设置回activeInstance变量中。 - 将
vm._vnode设置为了刚生成的vue-component-1组件的vnode。到这里我们总结一下,vm._vnoode中存储的是这个vm实例对应的虚拟节点,而vm.$vnode中存储的是父节点的虚拟节点。 - 然后执行
__patch__方法,传入了vm.$el和生成的vnode
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
function patch(oldVnode, vnode, hydrating, removeOnly){ ... }
在 patch 方法中:
- 根据传入的
oldVnode的nodeType判断其是否是一个真实元素。
在 Dom 中有很多 nodeType,比如元素节点的 nodeType 为 1,属性节点的 nodeType 为 2,文本节点的 nodeType 为 3,注释节点的 nodeType 为 8。。。
当前的 oldVnode 为 <div id="app"></div>,所以其为 nodeType 为 1 的真实元素。
- 如果
oldVnode是真实节点,那么需要进行转换为虚拟节点,最终得到的oldVnode就是根据真实节点生成的tag为div的虚拟节点,其中的elm属性保存了真实节点。 - 将
oldVnode.elm也就是传过来的vm.$el存入oldElm变量中,找到vm.$el的父元素存入parentElm变量中,当前为body元素。oldVnode是有elm属性的,但是vnode的elm属性还是undefined。 - 最后进入
createElm方法,传入通过_render生成的vnode(vue-component-1),oldVnode(div) 的父节点body和兄弟节点等。
-
调用
createElm方法 在这个方法里会调用和render过程同名的createComponent方法将vnode转换为真实的dom节点,为了清晰表示,以后render阶段的我们称为RcreateComponent,update阶段的我们称为UcreateComponent,。 -
调用
UcreateComponent方法
- 在
UcreateComponent方法中创建组件的时候,先查找vnode的data.hook.init方法,有的话就去执行,这个方法是我们在RcreateComponent方法里就存入的,也就是只有组件vnode有data.hook.init方法,因为只有组件才会进入这个方法,非组件直接执行的new Vnode()。 - 调用
init方法,传入vnode和false。 - 在
init方法中因为vnode.componentInstance不存在,所以调用了createComponentInstanceForVnode方法创建vnode.componentInstance,传入vnode和activeInstance(vm)。
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance))
- 执行
createComponentInstanceForVnode方法,这个方法中又执行了我们存入vnode的componentOptions属性中的Ctor方法,也就是extend期间创建的Sub这个VueComponent方法,可以对照前面生成的vnode来看。
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
new vnode.componentOptions.Ctor(options) // new VueComponent(options)
最后的 new VueComponent(options) 就进入子组件了,不要忘记,等子组件渲染完我们要回到这个地方继续执行的。