0%

Vue2的组件渲染

示例HTML

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
<!DOCTYPE html>
<html>

<head>
<title>Vue.js</title>
<!-- Delete ".min" for console warnings in development -->
<script src="../../dist/vue.js"></script>
</head>

<body>
<div id="app">
<ol>
<!-- Create an instance of the todo-item component -->
<todo-item></todo-item>
</ol>

</div>

<script>
Vue.component('todo-item', {
template: '<li>This is a todo</li>'
})
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
})
</script>
</body>

</html>

Vue.component发生了什么

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
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]

ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})

首先我们验证组件的名称是否合格,然后从继承base VueComponent构造器并根据当前的defination构造一个新的构造函数,并将这个组件的定义赋值给当前实例的options的components对象中,键是定义的组件名称,值为这个extend得来的构造函数。通过这种方式,我们将组件todo-item注册到当前vue实例上。

如何渲染子组件

Vue通过vm._update(vm._render(), hydrating)这条语句进行渲染,用_render生成vdom对象,并用_update方法内部调用patch方法渲染vnode到页面上去。vm._render实际上调用生成的render函数生成vnode对象。这里的由模板编译之后的render函数是这样的:

1
2
3
4
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('ol',[_c('todo-item')],1)])}
})

最终执行完render function返回一个vnode对象,这个vnode以div为根并包含了所有子vnode信息。我们拿到这个vnode去调用_update进行渲染,_update内部会调用patch方法传入新旧vnode进行比对渲染。这里的patch方法如下:

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}

let isInitialPatch = false
const insertedVnodeQueue = []

if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}

// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)

// create new node
// 这个是实际插入dom的地方
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}

// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}

在patch中,最核心的部分依然是createElm

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
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}

vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}

vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)

/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
// 关键所在
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}

if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}

这里我们调用createChildren对孩子vnode依次调用createElm创建dom节点,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
// 重点是对于每个子vnode,递归调用createElm插入dom节点
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}

我们注意到createElm中有这样一段逻辑:

1
2
3
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

而createComponent定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
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.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}

对于这条语句:

1
2
3
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}

当当前的vnode为组件时,如这里的todo-item,vnode.data.hook.init有定义,故执行init函数即这里的i函数,并传入vnode,而vue component的init函数定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
)
// 手动挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
}

keepalive暂不考虑,关注else的情况,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
// 实际上这个Ctor是VueComponent构造函数
return new vnode.componentOptions.Ctor(options)
}

对于这个vnode我们通过createComponentInstanceForVnode方法创造vue component实例, 在实例化这个Vue component实力的过程中,我们调用_init方法。实际上因为VueComponent继承自Vue,所以这里调用的_init方法实际上定义在Vue.prototype上:

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
function VueComponent (options) {
this._init(options)
}

Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
// 每一个组件有一个全局唯一_uid, 从0开始递增,Uid为0的组件是应用的跟组件
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

在实例化vue component的过程中,我们调用如beforeCreate和created这样的生命周期钩子。但是init并没有将组件mount,mount过程是在componentVnodeHooks中的init hook中完成,通过手动调用child.$mount(hydrating ? vnode.elm : undefined, hydrating)。我们知道child就是一个vue component实例,也继承自vue, 所以也是一个vue 实例,这里调用的$mount仍然存在于Vue.prototype上:

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
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)

/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}

我们在注册组件的时候提供了template: Vue.component('todo-item', { template: '<li>This is a todo</li>' }), 那么这里将template编译为render function,形式如下:

1
2
3
ƒ anonymous() {
with(this){return _c('li',[_v("This is a todo")])}
}

然后兜兜转转又开始重复我们初始化跟组件的过程,通过加入在线编译修改过后的$mount调用到原始的$mount方法,原始的$mount方法又调用mountComponent方法:

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
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el

callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

在mountComponent的过程中又继续实例化watcher,watcher实例化的过程中又调用updateComponent。即调用该组件的render function返回该组件对应的vnode对象,然后调用_update方法,_update进而调用patch方法来createElm,createElm进而createChildren,createChildren进而又createElm,将这个文本节点append到父li dom节点。最后patch方法返回这个li htmlelement节点赋值给vm.$el属性。

1
2
3
if (vm.$el) {
vm.$el.__vue__ = vm
}

htmlelement也可以反向引用到vue component实例

此时updateComponent调用完毕,vm的$el上也已有了对应的dom对象。对于todo-item组件,mountComponent的watcher实例化完毕,最终返回这个vm对象。init hook执行完毕,回到createComponent方法中:

1
2
3
4
5
6
7
8
9
10
11
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 将li的dom节点插入到父ol dom节点
// parentElm对应ol
// vnode.elm对应li
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}

此时vnode的componentInstance是vue component实例,最终createComponent返回true,意味着createElm提前终止,所以对于这个组件的vnode,执行流程与其他原生html元素会有所不同。然后就是递归的返回阶段,依此类推,将ol dom节点插入到div dom节点,然后将div dom节点插入到body,这一步完成之后,我们的This is a todo就显示在页面上了, 上面的这一切都发生在createElm方法中。

下面我们将回到根组件的patch方法中做一些清理工作,将旧的dom节点移除掉,然后执行inserted hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm

最后patch还是返回这个dom节点div#app,然后返回到updateComponent,watcher实例化完成,回到mountComponent,对应于根组件的mountComponent也完成了,触发mounted钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}

mounted结束之后,init过程也就结束了,这就是带有注册组件的Vue实例化的全过程了。

总结

可以看到,在实例化vue实例的时候,通过mountComponent->watcher->updateComponent->render function -> return vnode,然后update->patch -> crateElm -> createChildren,在这个过程中,不断createElm -> createChildren,如果孩子是Vuecomponent实例,则在createElm时提前通过createComponent初始化组件提前return。递归插入dom节点,最终赋给body,渲染到页面上。