Vue2 源码分析 -- 创建组件虚拟 DOM

207 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

组件的虚拟 DOM

在前面的分析中,我们分析了 render 函数转换成虚拟 DOM 的过程,对于遇到组件的情况,我们还没有分析。在 render 函数转换成虚拟 DOM 是,会对传入的标签字符串进行判断,如果是平台内置的标签,则创建虚拟 DOM;如果是已经注册的组件标签,则创建组件的虚拟 DOM

创建虚拟 DOM 基本流程

if (config.isReservedTag(tag)) {

  // platform built-in elements
  // 内置标签,直接创建虚拟 DOM
  vnode = new VNode(
    config.parsePlatformTagName(tag), data, children,
    undefined, undefined, context
  )
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  // component
  // 已经注册的自定义组件,创建组件类型的 VNode
  vnode = createComponent(Ctor, data, context, children, tag)
} else {
  // unknown or unlisted namespaced elements
  // check at runtime because it may get assigned a namespace when its
  // parent normalizes children
  vnode = new VNode(
    tag, data, children,
    undefined, undefined, contexts
  )
}

从上面的代码中, 如果为组件标签,则调用 createComponent 方法来创建组件的虚拟 DOMVue 中通过 resolveAsset 方法来判断组件是否已经注册,来看看 resolveAsset 方法的实现。

function resolveAsset (
 options: Object,
 type: string,
 id: string,
 warnMissing?: boolean
): any {
 /* istanbul ignore if */
 if (typeof id !== 'string') {
   return
 }
 const assets = options[type]
 // 这里多个分支对组件标签的多种形式进行判断(大小写、驼峰)
 if (hasOwn(assets, id)) return assets[id]
 const camelizedId = camelize(id)
 if (hasOwn(assets, camelizedId)) return assets[camelizedId]
 const PascalCaseId = capitalize(camelizedId)
 if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
 // fallback to prototype chain
 const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
 if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
   warn(
     'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
     options
   )
 }
 // 最后返回子类的构造器函数
 return res
}

通过上方代码可以发现,判断组件是否已经注册过程中,需要对标签的不同命名规范进行判断,最后返回子类构造器。在判定标签为组件标签之后,调用 createComponent 方法来创建组件的虚拟 DOM。我们来看一下 createComponent 方法的主要逻辑

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

  /**
   * 这里的 baseCtor 为 Vue 构造函数
   */
  const baseCtor = context.$options._base

  /**
   * 如果传入的构造器时一个对象
   * (一般情况下,创建一个 Vue 组件, export default 导出的都是一个对象,这是局部注册组件的情况,对于全局注册的组件, Ctor 是一个子类构造器函数)
   * 使用 extend 方法构造一个 Vue 的子类
   *  extend 方法内部使用的是原型继承
   */
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  /// 合并构造器配置
  resolveConstructorOptions(Ctor)


  // 安装组件钩子函数
  installComponentHooks(data)

  // 创建组件的虚拟 DOM , 名称以 vue-component 开头
  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
}

从上面的代码可以看出, 创建组件的虚拟 DOM 两个关键步骤就是合并选项和安装组件钩子函数。合并选项与 Vue 实例初始化是选项合并是基本一致的。我们来看下安装组件钩子函数做了些什么。

// 将 componentVNodeHooks 钩子函数合并到组件 data.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
    }
  }
}

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

至此, Vue 组件虚拟 DOM 创建的基本流程就已经分析完了。总结一下整体步骤

  • 判断标签为组件标签时,先通过判断时局部注册的组件还是全局注册的组件,对于局部注册的组件,此时的构造器为一个对象,需要通过 extend 方法将其转换成构造器函数

  • 合并选项配置,这里合并选项与根实例合并选项流程是一致的

  • 安装组件的钩子函数

  • 最后创建组件的虚拟 DOM