vue的源码学习之六——2.createComponent虚拟DOM组件

1. 介绍

        版本:2.5.17。 

       我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。

       学习文档:https://ustbhuangyi.github.io/vue-analysis/components/create-component.html

 2. 连接上一节

在上一节中我们从主线上把模板和数据如何渲染成最终的 DOM 的过程分析完毕了。

render(参生虚拟DOM) 是调用 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。 
scr/core/vnode/create-elements.js:

if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    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, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

在我们这一章传入的是一个 App 对象,它本质上是一个 Component 类型,那么它会走到上述代码的 else 逻辑,直接通过 createComponent 方法来创建 vnode。所以接下来我们来看一下 createComponent 方法的实现,它定义在 src/core/vdom/create-component.js 文件中

3. createComponent

src/core/vdom/create-component.js中:

export function createComponent (
  //可以是组件类型的类,函数或者对象
  Ctor: Class<Component> | Function | Object | void,
  // VNode(虚拟DOM)的相关Data
  data: ?VNodeData,
  // 上下文,也就是我们的当前的Vue实例---vm
  context: Component,
  // 组件的子VNode
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

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

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    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 || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  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
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder 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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

可以看到,createComponent 的逻辑也会有一些复杂,但是分析源码比较推荐的是只分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个 case 主要就 3 个关键步骤:构造子类构造函数,安装组件钩子函数和实例化 vnode

3.1 构造子类构造函数

const baseCtor = context.$options._base

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

 这里 export 的是一个对象,所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor)

import HelloWorld from './components/HelloWorld'

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

3.1.1 baseCtor就是Vue

在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:

Vue.options._base = Vue

 这里定义的是 Vue.option,而我们的 createComponent 取的是 context.$options,实际上在 src/core/instance/init.js 里 Vue 原型上的 _init 函数中有这么一段逻辑:

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

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

3.1.2 Vue.extend

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

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
**步骤1:**
    // vue调用该方法,所以this就是vue
    const Super = this
    const SuperId = Super.cid
    // 给当前的组件添加_Ctor做缓存使用,在Vue.extend这个函数的最后,会将子构造器缓存
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
**步骤2:**
    // 对组件的名字做校验,不能是h5的保留标签,如header
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
**步骤3:**
    // 创建子构造函数
    const Sub = function VueComponent (options) {
      //和Vue本身一样,需要_init(继承自Vue)
      this._init(options)
    }
    //子构造函数继承Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 子构造函数自己的配置和Vue的配置做合并
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This

**步骤4:**
    // 对子构造器的props和computed做初始化
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 将Vue的全局静态方法赋值给子构造器
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

**步骤5:**
    // cache constructor
    // 将子构造器缓存下来
    cachedCtors[SuperId] = Sub
    return Sub
  }

步骤1: 
super就是Vue, 
cachedCtors作为当前的组件添加_Ctor属性的属性值做缓存使用,在Vue.extend这个函数的最后,会将子构造器缓存

步骤2: 
对组件的名字做校验,不能是h5的保留标签,如header, 
利用的是validateComponentName方法,来自于、sc/core/utils/options

步骤3: 
创建子构造函数 
子构造函数继承Vue 
子构造函数自己的配置和Vue的配置做合并

步骤4: 
sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 
对sub 这个对象配置中的 props 和 computed 做了初始化工作

步骤5: 
将子构造器缓存下来,并存储给cachedCtors,每一个cachedCtors[SuperId]都对应一个组件,这样当不同的页面引用相同的组件的时候,只用创建一次该组件的子构造函数

总结: 
Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

3.2 安装组件钩子函数

installComponentHooks(data)

我们来看installComponentHooks函数,就在这个页面

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

是将我们的 data.hook (组件的data的钩子) 和 componentVNodeHooks 合并。

我们来看componentVNodeHooks是啥,也在该 js

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

componentVNodeHooks中存放的也是钩子,共有四个值init、prepatch、insert、destroy。具体实现后面讲解,从函数名上我们也可以猜到,它们分别是正在VNode对象初始化、patch之前、插入到dom中、VNode对象销毁时调用。

installComponentHooks函数将我们的 data.hook (组件的data的钩子) 和 componentVNodeHooks 合并的时候采用的是mergeHook函数方法

mergeHook:

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
}

其实就是如果 data.hook中有这个钩子,componentVNodeHooks也有这个钩子,那么就按照双方的逻辑各执行一遍。

3.3 实例化 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 的,并且会有组件的相关配置这个参数。

4. 总结

createComponent 在渲染一个组件的时候的 3 个关键逻辑:

  • 构造子类构造函数,
  • 安装组件钩子函数和实例化 vnode
  • createComponent 后返回的是组件 vnode。

它也一样走到 vm._update 方法,进而执行了 patch 函数,

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值