该小节对于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
后,会将其返回