@frank-shaw
2019-11-30T10:15:36.000000Z
字数 5948
阅读 1107
vue
源码
组件化机制
MVVM框架中,组件化是一种必要性的存在。
通过组件化,将页面做切割,并将其对应的逻辑做一定的抽象,封装成独立的模块。
那么Vue中的组件化是怎样做的呢?我们分几个点来具体阐述这个过程:组件声明过程Vue.component()
的具体实现,组件的创建与挂载过程。
Vue.component()
的声明是一个全局API,我们在/core/index.js
中查看函数initGlobalAPI()
的代码:
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
export function initGlobalAPI (Vue: GlobalAPI) {
//...省略
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
发现Vue.component()
的声明没有明确写在代码里,其声明的过程是动态的。具体过程在initAssetRegisters()
中:
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
// ASSET_TYPES : ['component','directive','filter']
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
//component的特殊处理
if (type === 'component' && isPlainObject(definition)) {
//指定name
definition.name = definition.name || id
//转换组件配置对象为构造函数
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
//全局注册:options['components'][id] = Ctor
//此处注册之后,就可以在全局其他地方使用
this.options[type + 's'][id] = definition
return definition
}
}
})
}
以一个例子来说明代码的过程吧:
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
组件的声明过程是:先获取组建的名称,然后将{data:"...", template: "...}
转变为构造函数,最后,将该组件的构造函数注册到 this.options.components
中。
当其他组件使用到该组件的时候,首先会在this.options.components
中查询是否存在该组件,由此实现了全局注册。
那么,我们想问:这个自定义组件,是在什么时候创建的?
首先创建的是根组件,首次_render()时,会得到整棵树的VNode结构,其中必然包括了子组件的创建过程。那么子组件的创建会是在哪一步呢?让我们来回顾根组件的创建过程:
new Vue() => $mount() => vm._render() => _createElement() => createComponent()
在_render()
的过程中,会触发_createElement()
。在该函数内部,会对是否是自定义组件进行查询,如果是自定义组件,那么会触发createComponent()
,其过程为:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...//省略
// 核心:vnode的生成过程
// 传入tag可能是原生的HTML标签,也可能是用户自定义标签
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 是原生保留标签,直接创建VNode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}else if((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options, 'components',tag))) {
// 查看this.options.components中是否存在对应的tag的构造函数声明
// 若存在,需要先创建组件,再创建VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...//省略
}
那么我们来看createComponent()
,它的主要工作是根据构造函数Ctor获取到VNode:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 省略...
// 安装组件的管理钩子到该节点上
// 这些管理钩子,会在根组件首次patch的时候调用
installComponentHooks(data)
// 返回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
)
// 省略...
return vnode
}
installComponentHooks()
具体做了什么事情呢?所谓的组件的管理钩子,到底是些啥东西啊?我们来具体看看:
const componentVNodeHooks = {
// 初始化钩子:创建组件实例、执行挂载
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
//创建自定义组件VueComponent实例,并和对应VNode相互关联
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
//创建之后,立刻执行挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
...
},
insert (vnode: MountedComponentVNode) {
...
},
destroy (vnode: MountedComponentVNode) {
...
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
//将自定义组件相关的hooks都放在生成的这个VNode的data.hook中
//在将来的根组件首次patch过程中,自定义组件通过这些hooks完成创建,并立即挂载
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
}
}
}
看代码可知,管理钩子一共有四个:init()
,prepatch()
,insert()
,destroy()
。看init()
的过程:创建自定义组件VueComponent实例,并和对应VNode相互关联,然后立刻执行$mount()
方法。
在installComponentHooks()
过程中,其实只是将这四个管理钩子放在生成的这个VNode的data.hook
中存放。对应的管理钩子调用,在首次执行update()时候,执行patch()
的过程里。详细的过程在/core/instance/vdom/patch.js
的createElm()
中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
//省略...
//子组件的生成过程
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
//原生标签的生成过程
//省略...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
//获取data
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.data.hook.init()方法,在此处创建自定义组件实例,并完成挂载工作
//详细见'./patch.js componentVNodeHooks()'
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
}
}
}
可以看到,自定义组件的创建过程在patch.js
的createComponent()
方法中。通过调用上面提到的vnode.data.hook.init()
方法(和文章的上一段联系起来),将自定义组件在此处创建,并且立即调用$mount()
。
这个时候就可以理解以下结论:
组件创建顺序自上而下
组件挂载顺序自下而上