# 组件化与patch
# 组件化
先来说说, 假如Vue中没有组件这个概念, 那用Vue实现一个页面, 将会是怎么样的
new Vue({
el: '#app',
data () {
return {
count: 0
}
},
template: `
<div>
<header>
title
</header>
<main>
<div>
<div>
{{count}}
</div>
<button type="button" @click="count++">+</button>
<button type="button" @click="count--">-</button>
</div>
</main>
<footer>
copyright
</footer>
</div>
`
})
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
Vue可以像一些模板引擎一样, 渲染template(也可以使用编程方式的render函数), 但是Vue的数据是响应式的, 当数据变化, 会触发模板重新渲染.
想象一下, 如果我们要用这种方式去写一个复杂的页面, 那Vue的template/render, data等将会非常复杂. Vue的组件化可以解决这个问题, 此外还可以达到组件复用的作用.
我们知道Vue实例可以维护模板和响应式数据. 既然组件化的一个目的是将模板拆分, 那么可以猜测, 拆分出来的模板都可以通过一个个Vue实例来维护? Vue正是这么做的, 一个个组件就是一个个Vue实例.
上面的例子, 我们可以将<main></main>
内部的内容分离成组件
const ButtonCount = {
name: 'ButtonCount',
data () {
return {
count: 0
}
},
render(h) {
return h('div',{}, [
h('div', {}, [this.count]),
h('button', {attr: {type: 'button'}, on: {click: ()=> this.count++}}, ['+']),
h('button', {attr: {type: 'button'}, on: {click: ()=> this.count--}}, ['-']),
])
}
}
new Vue({
el: '#app',
data () {
return {
count: 0
}
},
components: {
ButtonCount
},
render (h) {
return h('div', {}, [
h('header',{}, ['title']),
h('main',{}, [h('button-count')]),
h('footer',{}, ['copyright'])
])
}
})
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
这里我们定义了components
属性, 或者也可以使用Vue.component
, 因为我们在render函数中通过传递tag button-count
指代组件, Vue在render的时候会在vm.$options.components
(包括原型链)中去寻找组件(可以参考render部分). 而定义components
, 或使用Vue.component
,本质上是在往vm.$options.components
中去注入组件. 因此在.vue
文件中使用template
的时候, 我们通常需要使用components
属性来注册下组件.
当然也可以不需要手动注册组件, 直接利用作用域, 但是这种方式只能配合render函数使用
const ButtonCount = {
//...
}
new Vue({
el: '#app',
data () {
return {
count: 0
}
},
render (h) {
return h('div', {}, [
h('header',{}, ['title']),
h('main',{}, [h(ButtonCount)]),
h('footer',{}, ['copyright'])
])
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 组件化之后, 什么发生了变化?
我们拿上面的例子来说, 组件化之后, 首当其冲, render函数得到的vNode tree变了. 如果没有组件化, 我们只需要创建DOM元素对应的vNode, 组件化之后, 我们就需要创建组件对应的vNode, 参考render章节
# 组件化与patch
没有组件化的时候, 我们patch的过程会创建所有DOM元素, 并将vm.$el插入到DOM中. 组件化之后, 何时去创建组件对应的DOM元素呢?
case study
我们分析一个例子, 来了解组件化之后, patch的过程,
const Child1 = {
name: 'Child1',
render(h) {
return h('div', null, 'child1')
}
}
new Vue({
el: "#app",
render (h) {
return h('main',{class: 'main-class'}, [
'main-text',
h(Child1, {ref: 'child1'})
])
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app"></div>
前面的过程和非组件化的过程一致, 不同的是patch过程.
首先通过render得到vNode tree
这个例子中我们render得到到的tree 如下:
然后就需要根据这个vNode tree, 通过patch得到真实的DOM元素.
# 根组件patch
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
//...
} 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)) {
//...
}
// 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
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)) {
//,,,
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
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
上面的代码在patch章节分析过
关键代码在createElm
, 根据vnode创建DOM元素
const insertedVnodeQueue = []
// ...
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)
)
// createElm
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在我们的例子里, vnode
为render得到的vNode tree, insertedVnodeQueue
为空数组, parentElm
为body, refElm
只是用于往DOM中insert时的参照物.
# createElm
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') {
//...
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} 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)
}
}
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
对于DOM元素对应的vnode, createElm
执行的就是创建DOM元素的逻辑,
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
2
3
如果vnode对应着组件, 则createComponent
则返回true
, 直接return
对于我们的例子, 会先创建<main></main>
, 然后调用createChildren
来遍历子vnode, 并递归调用createElm
, 创建子vnode对应的DOM元素, 并插入到父vnode的vnode.elm
# createChildren
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
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)))
}
}
2
3
4
5
6
7
8
9
10
11
12
createChildren中调用createElm时, refElm传递的总是null
# createComponent
在我们的例子中, Child1
是组件vnode, createElm
中会执行createComponent
, 并返回true
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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
要判断一个组件是否是组件vnode, 关键判断其是否有vnode.componentInstance
, 创建组件的vnode时, 会为其注入hooks, 这其中就有init
, vnode.componentInstance
就是和init
hook 相关.
# init hook
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)
}
},
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
由于我们这不涉及keep-alive
, 在这个init钩子中, 主要实例化组件, 并进行挂载(empty mount), 实例化后的对象将赋值给vnode.componentInstance
TIP
挂载涉及$mount
, 但是如果是子组件的话, 不会在这里执行mounted钩子, 因为对于子组件来说, 其$vnode
不为null
, 这是在render的过程中决定的
src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# createComponentInstanceForVnode
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): 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
}
return new vnode.componentOptions.Ctor(options)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inlineTemplate涉及Vue的内联模板, 这里可以忽略
vnode.componentOptions.Ctor
是在render阶段就决定了的, 其来自于Vue.extend
, 所以接下去要实例化子组件.
new vnode.componentOptions.Ctor({
_isComponent: true,
_parentVnode: vnode,
parent
})
2
3
4
5
这里的_parentVnode
就是Child1 vnode, 我通常称其为子组件在父组件中的placeholder
而parent则是activeInstance
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
2
3
4
src/core/instance/lifecycle.js
export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
//...
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
//...
}
}
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
activeInstance
是在Vue.prototype._update
进行赋值的, 也就是在patch之前, 会将activeInstance
赋值为正在执行_update
, 即将patch的实例. 并在patch完后, 将其赋值回前一个值
在我们的例子中, 显然我们当前正在patch的是根组件,
new vnode.componentOptions.Ctor({
_isComponent: true,
_parentVnode: vnode,
parent
})
2
3
4
5
因此parent就是根组件的Vue实例
接下里就要实例化子组件, 下面看一些子组件实例化要执行的关键代码
src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
//...
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')
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
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
和实例化根组件还是有一些区别的, 首先子组件和根组件对vm.$options
的处理不同, 然后子组件由于没有指定el
参数, 因此不会自动挂载.
# initInternalComponent
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
// 内联模板相关, 忽略
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
initInternalComponent
主要是对vm.$options
进行处理,
vm.constructor.options
是合并了 Vue构造函数的options和子组件options的结果. 其合并过程在Vue.extend
中
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
//...
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
//...
return Sub
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TIP
Vue构造函数的options在initGlobalAPI(Vue)
中注入, 并受Vue.mixin()
影响
子组件的options则是我们传入的
回到initInternalComponent
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
2
3
4
5
6
7
首先使用原型链构造vm.$options
, 并设置相关属性
vm.$options.parent
为activeInstance
, 即当前正在执行_update
的实例, 我们的例子中是根组件的Vue实例,
vm.$options._parentVnode
是子组件在父组件中的placeholder, placeholder中有很多信息, 比如props, 监听的事件, 与slot相关的children(这些信息在render 组件对应vnode时, 保存在componentOptions
)
因此接下来的代码都是将这些placeholder上的信息, 保存在子组件实例的vm.$options
上
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
2
3
4
5
# child.$mount
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
2
3
4
5
在实例化完子组件之后, 就需要对子组件进行挂载(empty mount), 也就是执行其render和patch的逻辑
首先我们关注一下子组件的render过程
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack becaues all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
//...
} finally {
currentRenderingInstance = null
}
// ...
// set parent
vnode.parent = _parentVnode
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
在_render
中, _parentVnode
是子组件在父组件中的placeholder node, vm.$vnode
和vnode.parent
都指向了该placeholder, 因此子组件的vm.$vnode
一定是有值的, 并指向其placeholder, 而根组件的vm.$vnode
为undefined
render之后, 执行patch逻辑 src/core/instance/lifecycle.js
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
return 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 {
// ...
} else {
// ...
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vm.$el
为undefined
, 又回到了开头提到的patch
函数, 不过这次oldVnode
为undefined
, 因此代码执行的是子组件的empty mount
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
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') {
//...
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} 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)
}
}
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
与根组件不同, 子组件empty mount调用createElm
并没有传parentElm
, 根组件patch的时候parentElm
是body, 是将DOM元素插入到DOM中
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
2
3
4
5
6
7
8
9
10
11
由于子组件并没有传parentElm
, 因此其createElm
中, insert
并不会做任何操作, 子组件只会创建DOM元素, 并赋值给vnode.elm, 最终赋值给子组件的vm.$el
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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这样, 子组件patch的过程就算完成了. 子组件上面所有执行代码的源头都是createComponent
中的init
hook, init
hook中实例化了子组件并赋值给vnode.componentInstance
, 并执行empty mount, 从而得到子组件vNode tree对应的DOM元素vm.$el
# initComponent
initComponent(vnode, insertedVnodeQueue)
在init hook执行完之后,
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
isPatchable
在这里的作用可以参考附录
pendingInsert
的作用暂且不谈, 我们的例子中, 其为空数组, 因此insertedVnodeQueue
并没有变化, 还是空数组
initComponent
另一个很重要的作用, 是将子组件patch得到的vm.$el
赋值给vnode.elm
(vnode是子组件在父组件中的placeholder). 这样, placeholder vnode也拿到了子组件vnode tree对应的DOM元素.
TODO: setScope
我们的例子中, 会执行invokeCreateHooks
, 其作用在patch章节有提及. 我们的例子中, 往根组件注入$refs.child1
就是在其中执行.
new Vue({
el: "#app",
render (h) {
return h('main',{class: 'main-class'}, [
'main-text',
h(Child1, {ref: 'child1'})
])
}
})
2
3
4
5
6
7
8
9
组件对应的vnode(placeholder vnode)其在执行invokeCreateHooks
时, 相比DOM对应的vnode, 其会额外执行一些逻辑
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
2
3
4
5
6
7
8
9
10
这样, 我们的例子中, insertedVnodeQueue
首次push了一个vnode(Child1 vnode)
# insert
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
2
3
4
5
6
7
8
initComponent
中, 组件的placeholder vnode.elm
被赋值为vm.$el
我们就可以将其insert到parentElm(<main></main>
)中, 到这里createComponent
的逻辑都结束了.
从结果上来看, 当我们patch下面这个vnode tree
其流程如下,
createElm
<main></main>
createChildren
- createElm
main-text
-> insert to<main></main>
- createElm -> createComponent
<div>child1</div>
-> insert to<main></main>
- createElm
<div></div>
- createChildren -> createElm
child1
-> insert to<div></div>
- createElm
- createElm
insert to
<body></body>
remove
#app
显然这是一个深度优先遍历, 如果子组件中还有子组件, 会不断递归createElm
,createComponent
# invokeInsertHook
在根组件执行完上面的流程后, patch
函数中, 还有最后一个逻辑需要执行
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
根据我们上面的分析, 我们的例子中, insertedVnodeQueue
中只有一个vnode, 就是子组件Child1在父组件(根组件)中的placeholder, 即Child1 vnode
return 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 {
//...
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对于根组件(指定了el
属性)来说, 其isInitialPatch
为false
, 子组件其isInitialPatch
为true
因此根组件的invokeInsertHook
主要是执行insertedVnodeQueue
中vnode的insert hook
例子中, 是执行Child1的insert hook
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
不考虑keep-alive
, 首次挂载, 执行Child1的mounted
hook
Vue通过这种方式, 很好地协调了根组件和子组件/子子...组件的mounted
hook执行的顺序
# 根组件patch之后
src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
//...
} 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
}
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
根组件patch结束之后, DOM元素已经插入到DOM中, 执行根组件的mounted
钩子
之前有说明, 只有根组件的vm.$vnode
为undefined
# chart
# patch flowchart
# vnode,vm relation
# insertedVnodeQueue
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
2
3
4
5
6
7
8
9
10
11
return function patch (oldVnode, vnode, hydrating, removeOnly) {
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
2
3
4
前提1: 子组件在patch逻辑的最后, 会通过invokeInsertHook
将insertedVnodeQueue
赋值给其placeholder node(data.pendingInsert
),
前提2: 根组件会在patch逻辑的最后, 通过invokeInsertHook
, 调用insertedVnodeQueue
中vnode的insert hook
用分治归并的思想来理解, 根组件的insertedVnodeQueue
来源于嵌套的子组件的insertedVnodeQueue
, 其嵌套子组件的insertedVnodeQueue
来源于其嵌套子组件的insertedVnodeQueue
, 递归的终点是一个不嵌套子组件的组件, 其insertedVnodeQueue
为空数组. 结合下面例子的图来理解这个过程
递归式:
(当组件不嵌套组件)
insertedVnodeQueue
是空数组(当组件嵌套组件)
insertedVnodeQueue
为非空数组, 数组中的vnode来源有两个地方, 来源1是嵌套组件的placeholder node的data.pendingInsert
(前提1), 来源2是placeholder node本身
递归式2的来源1:
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
递归式2的来源2:
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
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)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode) }
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(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
根组件执行所收集的组件vnode的insert hook
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
//...
}
},
}
2
3
4
5
6
7
8
9
10
11
12
最后根组件执行自己的mounted hook
看下面的例子
const Child1_1 = {
name: 'Child1',
render(h) {
return h('div', null, '--child1_1')
},
mounted() {
console.log('mounted child1_1')
}
}
const Child1_2 = {
name: 'Child1',
render(h) {
return h('div', null, '--child1_2')
},
mounted() {
console.log('mounted child1_2')
}
}
const Child1 = {
name: 'Child2',
render(h) {
return h('div', null, ['-child1', h(Child1_1), h(Child1_2)])
},
mounted() {
console.log('mounted child1')
}
}
new Vue({
el: "#app",
render (h) {
return h('div',{class: 'root'}, [
'root',
h(Child1)
])
},
mounted () {
console.log('root mounted')
}
})
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
console output
// root created
// created child1
// created child1_1
// created child1_2
mounted child1_1
mounted child1_2
mounted child1
root mounted
2
3
4
5
6
7
8
# insertedVnodeQueue chart
结合下面的图, 来理解上面的过程, patch根组件的过程中, 会实例化子组件, patch子组件, 这个过程中收集insertedVnodeQueue, 从而组织mounted钩子的调用顺序
# 附录
# Vue组件为什么只能有一个根节点?
文章开头的例子是这样的
new Vue({
el: '#app',
data () {
return {
count: 0
}
},
template: `
<div>
<header>
title
</header>
<main>
<div>
<div>
{{count}}
</div>
<button type="button" @click="count++">+</button>
<button type="button" @click="count--">-</button>
</div>
</main>
<footer>
copyright
</footer>
</div>
`
})
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
后面, 我们把<main></main>
中的内容抽离成了组件, 这个例子里, main中的内容被div包裹了,
但是我们没办法将组件template写成如下的形式,
<template>
<div>
{{count}}
</div>
<button type="button" @click="count++">+</button>
<button type="button" @click="count--">-</button>
<template>
2
3
4
5
6
7
其本质原因是, Vue在数据变化后, 重新渲染视图的时候, 需要比对新老的virtual dom, 而目前的对比算法, 必须要求组件模板只有一个根节点
TODO: re-patch
# isPatchable
function isPatchable (vnode) {
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode
}
return isDef(vnode.tag)
}
2
3
4
5
6
对于DOM元素对应的vnode, 其判断tag是否有定义, 这样可以跳过comment vnode, text vnode
对于子组件对应的vnode, 由于vnode只是其在父组件的placeholder, 实例在vnode.componentInstance
上, 子组件的根节点就是vnode.componentInstance._vnode
, isPatchable
就是判断该节点的tag
# initComponent中为什么要用isPatchable来判断?
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
参考该commit
const vm = new Vue({
template: `
<div>
<test class="test"></test>
</div>
`,
components: {
test: {
data () {
return { ok: false }
},
template: '<div v-if="ok" id="ok" class="inner">test</div>'
}
}
}).$mount()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
因为子组件可能是不满足isPatchable的, 此时没必要执行invokeCreateHooks
的全部逻辑,
比如class的create hook, 会将placeholder vnode中的 class="test"
补充到子组件的class中, 如果this.ok
为false, 子组件是一个comment node, 这个添加class的操作就是不必要的.
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19