该小节对于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' } })
])
}
})
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生成返回,比如上例中的id为form-container和form-title的div,而组件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)
}
// ......
}
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
}
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
在实例化vm实例时会先进行参数合并,会对new Vue的参数对象和Vue.options进行合并,然后生成vm.$options,参数合并 (opens new window)小节有详细分析,可以查看对应的合并策略,所以,在createComponent中baseCtor指向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
}
}
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.prototype,Super指向Vue,this指向性的问题不过多说明,接着将Vue.options和传入的组件对象进行合并,需要注意的是和实例化Vue实例过程中的参数合并不一样,Vue.extend中的参数合并没有第三个参数,接着就是为构造函数Sub做一些属性扩展和对配置合并后的props和computed做初始化工作(代理),最后在方法内对构造函数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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
initProps、initComputed和Vue.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])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
initProps和initComputed方法主要是做了层代理处理,代理在构造函数的原型对象上,proxy方法和defineComputed方法分别在响应式对象 (opens new window)和计算属性 (opens new window)这两节中有过分析,这里不在赘述
需要注意的是,组件的Props和computed代理在Vue.extend阶段(因为组件实例是实例化Vue.extend返回的构造函数),根实例的Props和computed代理在初始化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)
// ......
}
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
}
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对象获取attrs和props数据,如果都为undefined则会直接返回空对象
如果attrs和props至少有一个存在数据,则会遍历组件对象的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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
checkProp方法逻辑较简单,其实就是在props和attrs中找到传递给组件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
}
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
}
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
组件vnode的tag都会有一个固定前缀vue-component,值得注意的是,实例化组件vnode时会传递{ Ctor, propsData, listeners, tag, children }选项对象且没有传递children,而实例化普通标签vnode时不会传递选项对象但会传递children,实例化组件vnode后,会将其返回