@frank-shaw
2019-11-30T02:15:36.000000Z
字数 5948
阅读 1412
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)) {//指定namedefinition.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] = definitionreturn 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, nsif (typeof tag === 'string') {let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)// 是原生保留标签,直接创建VNodeif (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的构造函数声明// 若存在,需要先创建组件,再创建VNodevnode = createComponent(Ctor, data, context, children, tag)} else {//vnode = new VNode(tag, data, children,undefined, undefined, context)}} else {// direct component options / constructorvnode = 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)// 返回VNodeconst name = Ctor.options.name || tagconst 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 flowcomponentVNodeHooks.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) {//获取datalet i = vnode.dataif (isDef(i)) {const isReactivated = isDef(vnode.componentInstance) && i.keepAliveif (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()。
这个时候就可以理解以下结论:
组件创建顺序自上而下
组件挂载顺序自下而上