我们通常在使用Vue开发时,难免会使用到组件component,那么在Vue中对于实现组件化的原理是怎么样的呢?为了让我们更好的使用Vue,了解其中的实现原理也是有必要的。
我们的主要目标是:组件是如何创建和挂载的?它和父组件的创建和挂载是个怎样的过程?组件的生命周期是怎样的?
我们先看一个简单的例子:
<body>
<div id='demo'>
<h1>vue组件化机制h1>
div>
<script>
Vue.component('comp', {
template: ' I am component'
})
const app = new Vue({
el: '#demo'
})
console.log(this.$options.reader)
script>
body>
输出:
"with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("虚拟DOM")]),_v(" "),
_c('comp') // 对于组件的处理并⽆特殊之处
],1)}"
发现,与普通的reader()没什么不同,对于vnode的结构与普通的节点元素基本是一致的。那么,Vue内部是如何对我们定义的组件进行转化和处理的呢?
根据我们在上一篇文章vue2源码解析(一) - new Vue()的初始化过程2.3.1小节中提到的初始化全局API方法,找到initGlobalAPI(Vue),进入该方法。所以,我们查看的起始文件为src/core/global-api/index.js
我们new Vue()时,会进行一系列的初始化操作,在初始化全局API的initGlobalAPI(Vue)方法中,为Vue的options初始化了components、filters和directives三个属性。
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
components、filters和directives三个选项,就是我们在new Vue()时,设置的那三个选项。
实现component()方法是在文件最下方的initAssetRegisters(Vue)中去做的,进入该方法对应的文件src/core/global-api/assets.js。
在initAssetRegisters()中,definition为我们Vue.component()的第二个参数。它的任务:
1)为component定义了name;
2)并根据definition继承Vue的构造函数,将该构造函数赋值给definition;Vue.extend({})为获取组件构造函数;
3)在Vue的options注册了components属性,这也是为什么我们能直接使用component的原因。但是,这里还并没有挂载。
4)最后返回definition。
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
this.options[type + 's'][id] = definition
return definition
也就是说,注册了一个Vue的全局API component,也为Vue的options设置了components属性,用户在使用components[id]就会对应设置上组件的构造函数,为后面创建组件对象做准备。
注意:组件的对象是VueComponent,是Vue的子类型,所以上面需要继承Vue的构造函数。
到这里,组件的API注册已经完成了。
由上一篇vue2源码解析(一) - new Vue()的初始化过程我们知道,Vue的初始化过程是:
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()
所以,接下来在Vue内部的处理流程是:在reader(h)的h中将传入的组件配置转化成vnode。reader(h)函数中h的定义是在src/core/instance/render.js
在前言部分的例子的输出内容中,我们发现:组件component的vnode结构与普通的节点元素基本是一致的。也就是说,都是调用_c()获取vnode。
而_c()其实和reader(h)中的h是同一个方法,也就是createElement()方法。_c()是给编译器使用的,h是给用户手写reader()时使用的。
进入createElement()方法对应的文件src/core/vdom/create-element.js。
观察源码发现createElement()调用_createElement(),在_createElement()中通过相关判断最后调用createComponent()去创建组件:
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
}
其中Ctor = resolveAsset(context.$options, ‘components’, tag))就是出从在Vue初始化全局API-组件注册过程中配置的选项中获取对应的组件构造函数。
进入createComponent()方法,对应的文件src/core/vdom/create-component.js
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
这里主要是处理组件的钩子函数:
1)进入installComponentHooks(data)方法
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
合并用户编写的钩子函数和系统默认的钩子函数。
2)查看hooksToMerge
const hooksToMerge = Object.keys(componentVNodeHooks)
作用:遍历系统默认钩子。
3)查看componentVNodeHooks
这里就不贴代码了,主要是系统默认钩子函数的定义,有4个钩子:
init():初始化、实例化和挂载组件;
prepatch():组件更新之前调用;
insert():组件被插入到父组件时调用;
destroy():组件销毁时调用。
我们这里主要是看init()方法做了什么,何时被调用。
4)init()的定义
在componentVNodeHooks中定义的init()方法会调用createComponentInstanceForVnode()方法根据生成的VNode创建组件实例,并调用$mount()挂载。
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
由上面的分析,我们知道了组件初始化方法定义的过程,那么组件的创建和挂载是何时进行的呢?子组件的生命周期和父组件的生命周期是什么关系呢?
我们再回顾一下new Vue()的初始化过程,new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update() => patch() => createElm()。
我们知道,patch()方法就是diff算法,作用是:初始化或遍历更新节点。所以,组件创建的init()方法应该是在patch()中调用。
因为通过上面的分析我们知道,组件component的vnode结构与普通的节点元素基本是一致的。也就是说,子组件是父组件的一部分,和普通节点一样,都是父组件在创建时通过patch()函数遍历生成的真实子节点。所以,肯定是“先父后子”的创建过程。
我们通过源码去验证一下我们的推断:
由于子组件在父组件中与普通元素节点无异,所以,也是通过createElm()递归遍历创建出真实dom的。进入createElm()方法,对应文件src/core/vdom/patch.js。找到了:
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
进入createComponent():
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const 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
}
}
}
这里会判断组件实例是否存活,不存活则调用init()方法创建并挂载组件实例。
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
所以,我们的推断是准确的!“先父后子”的创建过程,“由内而外”的挂载过程。
new Vue()的初始化过程中的initGlobalAPI()
在父组件创建后,未挂载前。在patch()中创建和挂载。
创建过程:先父后子。挂载过程:先子后父。
parent.created => parent.beforeMounted => child.created => child.beforeMount => child.mounted… => parent.mounted…