创建子组件实例
创建组件实例一节中我们知道,根组件是用户显式调用new Vue()
创建的 Vue 实例。除根组件实例之外的 Vue 实例,我们统称为子组件实例。而子组件,都是在根组件patch
的过程中创建的。
PS:一般所说的组件,都是指子组件,当指根组件时,会强调是根组件。
当调用createElm
为 VNode 创建对应的 DOM 节点时,会先判断该 VNode 是否是组件占位节点。如果是,则创建组件实例,并结束createElm
的过程;否则,继续为非组件占位 VNode 创建对应的 DOM 元素/文本/注释节点。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
// 组件占位 VNode:创建组件实例以及创建整个组件的 DOM Tree,(若 parentElm 存在)并插入到父元素上
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
createComponent
createComponent
主要负责创建组件占位 VNode 的组件实例并做一些事后处理工作,而对于非组件占位 VNode 将不做任何操作并返回undefined
。
我们在为组件创建组件占位 VNode 时,会在组件占位 VNode 的vnode.data.hook
上安装一系列的组件管理钩子方法,其中就存在init
钩子。
若传入的 VNode 是组件占位 VNode,则将存在vnode.data.hook.init()
钩子,调用init
钩子后,将为组件占位 VNode 创建组件实例vnode.componentInstance
。因此针对组件占位 VNode,createComponent
函数最终将返回true
,以表明该传入的 VNode 是组件占位 VNode,并完成了组件实例的创建工作。
反之,若传入的 VNode 不是组件占位 VNode,则不会存在vnode.data.hook.init()
钩子,更加不会创建出组件实例vnode.componentInstance
,因此最终createComponent
函数将返回undefined
,createElm
函数将继续往下执行,为非组件占位 VNode 创建对应的 DOM 节点。
createComponent
的主要流程为:
- 若 VNode 存在
vnode.data.hook.init
方法,说明是组件占位 VNode,则创建组件实例,挂在vnode.componentInstance
上 - 若
vnode.componentInstance
存在- 初始化组件实例,设置
vnode.elm
- 将组件的 DOM Tree 插入到父元素上
- 返回 true
- 初始化组件实例,设置
- 针对非组件占位 VNode,返回
undefined
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
// ...
/**
* 创建组件占位 VNode 的组件实例
* @param {*} vnode 组件占位 VNode
* @param {*} insertedVnodeQueue
* @param {*} parentElm DOM 父元素节点
* @param {*} refElm DOM nextSibling 元素节点,如果存在,组件将插入到 parentElm 之下,refElm 之前
*/
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 是否是重新激活的节点(keep-alive 的组件 activated 了)
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 若是 vnode.data.hook.init 存在(该方法是在 create-component.js 里创建组件的 Vnode 时添加的)
// 说明是组件占位 VNode,则调用 init 方法创建组件实例 vnode.componentInstance
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// 注释翻译:
// 若是该 VNode 是子组件(的占位 VNode),调用 init 钩子方法后,该 VNode 将创建子组件实例并挂载了
// 子组件也设置了占位 VNode 的 vnode.elm。此种情况,我们就能返回 true 表明完成了组件实例的创建。
if (isDef(vnode.componentInstance)) {
// 初始化组件实例
initComponent(vnode, insertedVnodeQueue)
// 将组件 DOM 根节点插入到父元素下
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
// ...
}
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
vnode.data.hook.init
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 创建子组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 对于正常的子组件初始化,会执行 $mount(undefined)
// 这样将创建组件的渲染 VNode 并创建其 DOM Tree,但是不会将 DOM Tree 插入到父元素上
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
}
/**
* 创建子组件实例
* @param {*} vnode 组件占位 VNode
* @param {*} parent 创建该组件时,处于活动状态的父组件,如此形成组件链
*/
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
// 创建子组件实例时,传入的 options 选项
const options: InternalComponentOptions = {
// 标明是内部子组件,在调用组件的 _init 初始化时,将采用简单的配置合并策略
_isComponent: true,
// 组件的占位 VNode
_parentVnode: vnode,
// 当前处于活动状态的父组件
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
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
init
钩子方法里,会先调用createComponentInstanceForVnode
创建子组件的实例。
在createComponentInstanceForVnode
函数里,vnode.componentOptions.Ctor
是在为组件创建 VNode 时传入的组件构造函数,该构造函数是基于Vue
构造函数继承而来,并混合了组件自身的选项在Ctor.options
里。此外,创建实例时,也会往Ctor
里传入options
选项,但是这个options
跟创建根组件传入的options
有些许区别。
_isComponent: true
:用来标明这个组件是内部子组件,在调用组件的_init
方法初始化时,将采用简单的配置合并,详见合并配置 - 子组件_parentVnode: vnode
:vnode
是当前子组件实例的占位 VNode,用于在后续合并配置时将组件实例跟组件占位 VNode 联系起来parent
:创建当前子组件实例时,处于活动状态的父组件
new vnode.componentOptions.Ctor(options)
将生成组件实例,并调用vm._init
方法对组件实例做初始化工作后返回组件实例。
init
钩子里,创建完子组件实例之后,会将子组件实例赋给vnode.componentInstance
,这样的话,组件占位 VNode 和组件实例就联系了起来。之后,调用子组件实例的$mount
方法,但是传入的第一个参数为undefined
,子组件实例将调用vm._render
方法生成渲染 VNode,并调用vm._update
进而调用vm.__patch__
创建组件的 DOM Tree,但是不会将 DOM Tree 插入到父元素上,插入到父元素的操作将在初始化子组件实例时完成,请见下一节。
重要提示
此处创建子组件的实例时,会创建子组件的渲染 VNode 并创建子组件的 DOM Tree。若是子组件里有子孙组件,也会递归创建子孙组件的实例、创建子孙组件的渲染 VNode,并创建子孙组件的 DOM Tree。
initComponent
/**
* 初始化组件实例
*/
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
// 将子组件在创建过程中新增的所有节点加入到 insertedVnodeQueue 中
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
// 获取到组件实例的 DOM 根元素节点
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
// 调用 create 钩子
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
/**
* 判断 vnode 是否是可 patch 的:若组件的根 DOM 元素节点,则返回 true
*/
function isPatchable (vnode) {
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode
}
// 经过 while 循环后,vnode 是一开始传入的 vnode 的首个非组件节点对应的 vnode
return isDef(vnode.tag)
}
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
初始化组件实例过程中,需要做比较多的工作:
- 将子组件首次渲染创建 DOM Tree 过程中收集的
insertedVnodeQueue
(保存在子组件占位 VNode 的vnode.data.pendingInsert
里)添加到父组件的insertedVnodeQueue
,详见Patch - insertedVnodeQueue 的作用 - 获取到组件实例的 DOM 根元素节点,赋给
vnode.elm
- 判断组件是否是可
patch
的- 组件可
patch
- 调用
create
钩子,详见patch 辅助函数 - invokeCreateHooks - 设置
scope
- 调用
- 组件不可
patch
- 注册组件的
ref
- 将组件占位 VNode 加入到
insertedVnodeQueue
- 注册组件的
- 组件可
VNode 不可 patch 的情况
在判断组件是否可patch
时,判断的依据是组件的 DOM Tree 的根节点是否是元素节点。在模板编译时,当组件模板的根节点不是元素节点时,编译会报错;但是在用户手写的render
函数里,可以给createElement
传入falsy value
,比如''
/null
/undefined
,此时createElement
会返回个注释类型 VNode。
<template>
<div id="app">
<HelloWorld ref="hello" :hello="a"></HelloWorld>
</div>
</template>
<script>
export default {
name: 'App',
components: {
HelloWorld: {
name: 'HelloWorld',
data () {
return {
}
},
render (h) {
// return h(null)
// return h(undefind)
return h('')
}
}
},
mounted () {
console.log(this.$refs.hello)
}
}
</script>
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
上面的组件的根节点就是个注释类型的 VNode,但是仍要保留组件的ref
以及执行组件在插入父元素上时的insert
钩子。
组件 DOM Tree 插入到父元素
当组件创建好并初始化好组件实例之后,其 DOM Tree 也已经完全 ready,此时若是存在parentElm
,就会将组件的 DOM Tree 插入到parentElm
。若是该组件同时作为其他组件渲染 VNode 的根节点,则不会存在parentElm
,也不会插入到parentElm
。详见:组件的 DOM Tree 是如何插入到父元素上的?