# 组件化

组件化,就是把页面拆分为多个组件,组件是资源独立的,组件在系统内可复用,组件和组件之间可以嵌套。

# createComponent

createElement 最终会调用 _createElement 方法,其中有一段对 tag 的判断,如果是一个 string,会实例化为一个普通 VNode 节点或未知类型节点,否则通过 createComponent 方法创建一个组件 VNode。

传入的是一个App对象时,直接通过 createComponent 方法创建 vnode。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {}

src/core/vdom/create-component.js 中主要有以下三个步骤:

# 构造子类构造函数

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

import HelloWorld from './components/HelloWorld'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}

这里export了一个对象,createComponent 中就会执行到 baseCtor.extend(Ctor),baseCtor就是 Vue,这是 src/core/global-api/index.js 中 initGlobalAPI 函数的逻辑:

Vue.options._base = Vue

这里定义了options,之前的 createComponent 取自 context.$options,在 src/core/instance/init.js 原型上的 _init 函数中有相关逻辑:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

Vue.extend 的定义

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super
  ...
  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}

将一个纯对象转化为一个基于 Vue 的构造器 Sub 并返回,并对其进行拓展,最后将该构造器针对其Id进行缓存避免重复构造。

# 安装组件钩子函数

Vue的vdom参考了开源库snabbdom,它的特点是在patch流程中对外暴露了各种时机的钩子函数。

installComponentHooks(data)

src/core/vdom/create-component.js

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {}
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {}
  insert (vnode: MountedComponentVNode) {}
  destroy (vnode: MountedComponentVNode) {}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

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
    }
  }
}

installComponentHooks 就是将 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关钩子函数,合并时如果某个时机的钩子已经存在 data.hook 中,那么通过 mergeHook 做合并。

# 实例化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

通过 new VNode实例化一个 vnode 并返回。与普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的。

createComponent 会返回 vnode,同样会执行 vm._update 方法,进而执行 patch 函数。

# patch

patch 的过程会调用 createElm 创建元素节点,定义在 src/core/vdom/patch.js

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // 这里又会出现一个 createComponent
  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
    }
  }
}

首先对 vnode.data 做判断,如果 vnode 是一个 VNode,则得到 i 是钩子函数。在创建组件 VNode 时合并的钩子函数中包含 init 钩子函数。(上一章介绍过) 该函数通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件。

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

这里的 new vnode.componentOptions.Ctor(options) 相当于上节的 new Sub(options)

Last Updated: 2020/6/9 下午5:19:29