[Vue源码学习] _render(下)

339 阅读3分钟

系列文章

前言

在上一章节中,我们可以通过createElement方法,创建普通元素VNode,那么在本章节中,我们就来看看Vue是如何创建组件VNode的。

_createElement

_createElement方法中,有两种创建组件VNode的方式:

/* core/vdom/create-element.js */
export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // ...
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
}
  1. tag是一个字符串,并且不是平台内置标签,就会通过resolveAsset方法,尝试通过tag$options.components中找到组件的定义:

    /* core/util/options.js */
    export function resolveAsset(
      options: Object,
      type: string,
      id: string,
      warnMissing?: boolean
    ): any {
      /* istanbul ignore if */
      if (typeof id !== 'string') {
        return
      }
      const assets = options[type]
      // check local registration variations first
      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方法,创建组件VNode

  2. tag是一个对象,直接调用createComponent方法,创建组件VNode

那么接下来,我们就来看看createComponent方法的具体实现。

createComponent

createComponent方法的代码如下所示:

/* core/vdom/create-component.js */
export 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
  }

  const baseCtor = context.$options._base

  // 通过组件配置对象生成组件构造器
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // 异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // 在Super.options更新时,重新调用mergeOptions方法,生成Sub.options
  resolveConstructorOptions(Ctor)

  // 处理v-model指令
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 从data.attrs和data.props中提取组件prop
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // 函数组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // 保存组件自定义事件后,交换native事件
  const listeners = data.on
  data.on = data.nativeOn

  // 抽象组件
  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 注册组件通用钩子函数
  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
}

createComponent方法中,首先会调用extend方法,通过组件配置对象生成组件构造器,这部分的内容在配置合并章节中已经进行过详细介绍;在看接下来的逻辑之前,首先需要明确一件事情,这里创建的组件节点,是在父组件执行render的过程中生成的,它只是一个单纯的组件占位符节点,此时不会解析该组件内部的渲染内容,并且对于这个父占位符节点来说,到真正需要实例化子组件的时候,要为子实例提供propsDatalistenerschildren数据,所以接下来的逻辑,主要就是构建这几项数据,所以在createComponent方法中,首先会从data中提取propsDatalisteners,然后调用installComponentHooks方法,向占位符节点注册通用的钩子函数:

/* core/vdom/create-component.js */
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
    }
  }
}

const hooksToMerge = Object.keys(componentVNodeHooks)

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // ...
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // ...
  },

  insert(vnode: MountedComponentVNode) {
    // ...
  },

  destroy(vnode: MountedComponentVNode) {
    // ...
  }
}

可以看到,这部分逻辑主要就是在组件的父占位符节点的data.hook上注册initprepatchinsertdestroy钩子函数,这样组件在之后的初始化,更新,卸载等过程中,就可以通过这些钩子函数,做相应的操作,注册完钩子函数后,就调用VNode构造函数,创建组件VNode,需要注意的是,之后传给子实例propsDatalistenerschildren,是作为componentOptions保存在组件VNode中的,最终,将创建好的节点返回。

总结

通过createComponent方法,就可以创建组件VNode,同时将与之相关的数据,比如组件构造器Ctor、属性propsData、自定义事件listeners、插槽children等,保存在componentOptions中,最后还给该组件各个阶段注册了钩子函数,以便Vue能够统一控制组件的创建、更新、销毁过程。