createComponent

11/4/2021 vue

该小节对于createComponent方法只会分析主要逻辑部分,对于异步组件、函数式组件、自定义组件v-model、keep-alive分支逻辑先暂时略过,后续会出专门小节分析

render (opens new window)小节分析过,render方法返回的vnode是通过调用_createElement内部方法而来

会借助如下例子做后续逻辑的分析

new Vue({
  el: '#app',
  render (h) {
    return h('div', {
        attr: {
          id: 'form-container'
        }
      }, [
      h('div', {
        attr: {
          id: 'form-title'
        }
      }, '表单'),
      h({
        template: `
          <div class="form-wrapper">
            <div class="form-item">
              <label for="name">姓名:</label>
              <input :value="name" id="name" />
            </div>
            <div class="form-item">
              <label for="age">年龄:</label>
              <input :value="age" id="age" />
            </div>
          </div>
        `,
        data () {
          return {
            name: '张三'
          }
        }
      }, { props: { age: 20 }, attrs: { id: 'foo' } })
    ])
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

普通标签vnode_createElement方法内部直接通过new VNode生成返回,比如上例中的idform-containerform-titlediv,而组件vnode则是通过在_createElement方法内再调用createComponent方法生成

_createElement方法内有这一段逻辑

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
	
  // ......
	
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      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 = 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)
  }

  // ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

这段逻辑很简单,判断tag是否为字符串,如果为字符串且为内置的元素,则和生成普通标签vnode一样,直接new VNode,接着会判断tag是否为已注册的组件名称,如果是则会调用createComponent方法生成vnode(组件查找这段逻辑可在组件注册 (opens new window)小节查看),如果tag不是字符串则直接调用createComponent方法生成vnode

在所写例子的参数对象render方法中,我们在调用参数h方法(其实就是调用vm.$createElement方法)时传入了一个组件对象,所以会走对tag类型判断的else分支,会直接调用createComponent方法,tag就是参数对象

createComponent方法定义在一个单独的文件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
  }

  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
  /*
    异步组件部分
  */

  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
  /*
    自定义组件v-model部分
  */

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

  // functional component
  /*
    函数式组件部分
  */

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


  /*
    抽象组件部分 keep-alive
  */


  // 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 */
  /*
	  weex部分
  */

  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

在该方法内部首先注意的是const baseCtor = context.$options._base这段代码,在src/core/global-api/index.js文件中存在这样一行代码

Vue.options._base = Vue
1

在实例化vm实例时会先进行参数合并,会对new Vue的参数对象和Vue.options进行合并,然后生成vm.$options参数合并 (opens new window)小节有详细分析,可以查看对应的合并策略,所以,在createComponentbaseCtor指向Vue构造函数

经过上面的逻辑分析调用createComponent方法时,Ctor就是我们传入的组件对象,会接着调用baseCtor.extend方法,实际上就是调用Vue.extend方法

# Vue.extend

Vue.extend方法在单独的文件src/core/global-api/extend.js中定义

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  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.cid = cid++  
    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
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    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)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

Vue.extend的逻辑很清晰,其实就是根据传入的参数对象在该方法内创建一个构造函数Sub并返回,创建的构造函数原型继承自Vue.prototypeSuper指向Vuethis指向性的问题不过多说明,接着将Vue.options和传入的组件对象进行合并,需要注意的是和实例化Vue实例过程中的参数合并不一样,Vue.extend中的参数合并没有第三个参数,接着就是为构造函数Sub做一些属性扩展和对配置合并后的propscomputed做初始化工作(代理),最后在方法内对构造函数Sub做了缓存处理,避免重复创建构造函数

Vue.extend = function (extendOptions: Object): Function {
  // ......
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
	
  // ......
	
  // prop和computed代理
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // .......	

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

initProps、initComputedVue.extend定义在同一文件中

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

initPropsinitComputed方法主要是做了层代理处理,代理在构造函数的原型对象上,proxy方法和defineComputed方法分别在响应式对象 (opens new window)计算属性 (opens new window)这两节中有过分析,这里不在赘述

需要注意的是,组件的Propscomputed代理在Vue.extend阶段(因为组件实例是实例化Vue.extend返回的构造函数),根实例的Propscomputed代理在初始化state阶段,组件是代理在组件实例的原型对象上,而根实例是代理在实例对象上

继续回到createComponent方法中,Ctor则为Vue.extend返回的构造函数,接着会调用extractPropsFromVNodeData方法提取Props

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
	
  // ......
	
  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)
	
  // ......
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# extractPropsFromVNodeData

定义在单独的文件src/core/vdom/helpers/extract-props.js

export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  // we are only extracting raw values here.
  // validation and default values are handled in the child
  // component itself.
  const propOptions = Ctor.options.props
  if (isUndef(propOptions)) {
    return
  }
  const res = {}
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    for (const key in propOptions) {
      const altKey = hyphenate(key)
      if (process.env.NODE_ENV !== 'production') {
        const keyInLowerCase = key.toLowerCase()
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          tip(
            `Prop "${keyInLowerCase}" is passed to component ` +
            `${formatComponentName(tag || Ctor)}, but the declared prop name is` +
            ` "${key}". ` +
            `Note that HTML attributes are case-insensitive and camelCased ` +
            `props need to use their kebab-case equivalents when using in-DOM ` +
            `templates. You should probably use "${altKey}" instead of "${key}".`
          )
        }
      }
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

该方法主要是提取PropsData,逻辑很清晰,借助上面所写例子进行分析,如果组件对象没有Props,则会直接return undefined,接着会根据传入data对象获取attrsprops数据,如果都为undefined则会直接返回空对象

如果attrsprops至少有一个存在数据,则会遍历组件对象的Props,获取每一个Prop名称(在参数项合并过程中会对Props规范化处理,将每个Prop名称转为驼峰形式),在调用checkProp之前会先做一层判断,如果定义的每个Prop名称和其小写形式不相同,且存在attrs对象,并且小写形式的Prop名称存在于attrs中,则会报出错误提示信息。

其实就是Prop大小写问题,组件中定义的Prop名称如果是驼峰形式,那么在DOM模板中使用时,传递数据的Prop名称必须是带有短横线分割的形式,在Vue文档Prop (opens new window)一节中也有对应的说明

接着会调用checkProp方法

function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

checkProp方法逻辑较简单,其实就是在propsattrs中找到传递给组件Prop的数据,然后添加到res对象中,接着在extractPropsFromVNodeData方法中返回res对象

继续回到createComponent的后续逻辑

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  
	// ......
	
  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // ......

  data = data || {}

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

  // ......

  // 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 */
  
  /*
	  weex部分
  */

  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

接着会调用installComponentHooks方法安装组件钩子函数

# installComponentHooks

installComponentHooks方法和createComponent方法定义在同一文件中

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

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

installComponentHooks方法其实就是将componentVNodeHooks中定义的钩子函数添加到data.hook中,在添加过程中,如果data.hook中已经存在对应的钩子函数,则会利用mergeHook方法进行合并,在适当的实际会依次调用合并的钩子函数

接着分析createComponent中的后续逻辑,实例化组件vnode,也就是占位符vnode

# 实例化组件vnode

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  
	// ......
	
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // ......

  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

组件vnodetag都会有一个固定前缀vue-component,值得注意的是,实例化组件vnode时会传递{ Ctor, propsData, listeners, tag, children }选项对象且没有传递children,而实例化普通标签vnode时不会传递选项对象但会传递children,实例化组件vnode后,会将其返回

最后更新时间: 12/4/2022, 1:44:46 PM