Vue源码学习2.1:createComponent

1,565 阅读3分钟

ps:建议PC端观看,移动端代码高亮错乱

这张我们开始进入组件化的学习,上一章我们在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode

// src/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') {
    // ...
  } else {
    vnode = createComponent(tag, data, context, children);
  }
}

比如说当我们使用 vue-cli 脚手架开发时,我们这样 new Vue

new Vue({
  renderh => h(App),
}).$mount('#app')

我们给 createElement 传入的是一个组件,会走到 else 逻辑,调用 createComponent 创建一个组件

接下来看看 createComponent 的真面目,定义在 src/core/vdom/create-component.js

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

  // 核心逻辑1:创建子类构造函数
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // 暂时不需要关心的其他逻辑:
  // 1. 异步组件
  // 2. 如果在创建组件构造函数之后应用了全局mixin,则解析构造函数options
  // 3. 将组件 v-model 转换成 props & events
  // 4. 提取props
  // 5. 函数式组件
  // 6. 对事件监听的处理
  // 7. 抽象组件处理

  // 核心逻辑2:安装组件钩子函数
  installComponentHooks(data)

  // 核心逻辑3:实例化 VNode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex的一些逻辑...

  return vnode
}

注释中标注了三个需要在本章中关注的核心逻辑:

  • 创建子类构造函数
  • 安装组件钩子函数
  • 实例化 VNode

1. 创建子类构造函数

首先需要知道 baseCtor 就是大 Vue 构造函数,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数中:

Vue.options._base = Vue

细心的同学会发现,这里定义的是 Vue.options,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.js 里的 _init 函数中有这么一段逻辑:

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

这样就把 Vue 上的一些 option 扩展到了 vm.$options 上,所以我们也就能通过 vm.$options._base 拿到 Vue 这个构造函数了。mergeOptions 的实现我们会在后续章节中具体分析,现在只需要理解它的功能是Vue 构造函数的 options 和用户传入的 options 做一层合并到 vm.$options 上。

另外,我们的组件通常都是一个普通的对象,比如通过 vue-loader 对我们的单文件组件处理以后返回的就是一个普通的对象

所以 isObject(Ctor) 为真,然后通过 baseCtor.extend(Ctor) 创建构造函数,也就是 Vue.extend

1.1 Vue.extend

Vue.extend 函数定义在 initExtend 中,initExtend 定义在 initGlobalAPI中。

// src/core/global-api/extend.js

export function initExtend (Vue: GlobalAPI{
  // 每个构造函数实例,包括Vue,都有唯一的cid,可以用于缓存子构造函数
  Vue.cid = 0
  let cid = 1

  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) // 验证name
    }

    const Sub = function VueComponent (options{
      // 执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。
      this._init(options)
    }
    // 原型继承
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // ...

    // 缓存构造函数
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
  • 定义子类构造函数 Sub,基于原型链继承于 Vue
  • 然后对 Sub 这个对象本身扩展了一些属性,如:
    • 扩展 options,添加全局 API
    • 对配置中的 propscomputed 做了初始化工作。
    • 关于 initProps 还有会专门的章节来介绍,这里留个印象
  • 缓存构造函数,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,实例化子组件的逻辑在之后的章节会介绍。

const Sub = function VueComponent (options{
  this._init(options)
}

2. 安装组件钩子函数

安装的作用就是在 VNode 执行 patch 的过程中执行相关的钩子函数,具体的执行我们稍后在介绍 patch 过程中会详细介绍。

installComponentHooks 函数的逻辑:

// src/core/vdom/create-component.js
function installComponentHooks (data: VNodeData{
  const hooks = data.hook || (data.hook = {})
  // hooksToMerge 是 Object.keys(componentVNodeHooks)
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i] // init, prepatch, insert, destroy
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

要看懂这个函数,还需要了解两个东西:

  • componentVNodeHooks对象
  • mergeHook函数

2.1 componentVNodeHooks

Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNodepatch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js 也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:

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

2.2 mergeHook

// src/core/vdom/create-component.js
function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

mergeHook 函数逻辑很简单,所谓合并就是先执行 componentVNodeHooks 定义的再执行 data.hooks 定义的,再将合并标志位设为 true

3. 实例化 VNode

// src/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 
{
  // ...

  // 核心逻辑3:实例化 VNode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
  • 通过 new VNode 实例化一个 vnode 并返回。
  • 和普通元素节点的 vnode 不同,组件的 vnode 是没有 children,这点很关键,在之后的 patch 过程中我们会再提。
  • 第七个参数是 componentOptions ,在 patch 过程中可以通过 new vnode.componentOptions.Ctor 来实例化子组件构造函数

总结

这一节我们分析了 createComponent 的实现,了解到它在渲染一个组件的时候的 3 个关键逻辑:创建子类构造函数安装组件钩子函数实例化 vnodecreateComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数,我们在上一章对 patch 函数做了简单的分析,那么下一节我们会对它做进一步的分析。