Vue源码阅读(七):组件化机制的实现

1,605 阅读4分钟

--本文采自本人公众号《猴哥别瞎说》

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过程

首先创建的是根组件,首次_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()方法。

update过程

installComponentHooks()过程中,其实只是将这四个管理钩子放在生成的这个VNode的data.hook中存放。对应的管理钩子调用,在首次执行update()时候,执行patch()的过程里。详细的过程在/core/instance/vdom/patch.jscreateElm()中:

  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.jscreateComponent()方法中。通过调用上面提到的vnode.data.hook.init()方法(和文章的上一段联系起来),将自定义组件在此处创建,并且立即调用$mount()

这个时候就可以理解以下结论:

组件创建顺序自上而下

组件挂载顺序自下而上


vue源码解读文章目录:

(一):Vue构造函数与初始化过程

(二):数据响应式与实现

(三):数组的响应式处理

(四):Vue的异步更新队列

(五):虚拟DOM的引入

(六):数据更新算法--patch算法

(七):组件化机制的实现

(八):计算属性与侦听属性

(九):编译过程的 optimize 阶段

Vue 更多系列:

Vue的错误处理机制

以手写代码的方式解析 Vue 的工作过程

Vue Router的手写实现