0%

Vue VNode Patch 分析

这里记录一下Vue的 Virtual DOM 比较过程
来自于 cn.vuejs.org patchVnode 函数断点

当我们对于data进行修改之后会产生新的 VDOM 集合
这里是 vnode

oldVnode则代表修改之前的 VDOM

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
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}

if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}

const elm = vnode.elm = oldVnode.elm

if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}

// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}

let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}

const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}

Vue 会在 updateChildren 中比较 VNode
每次调用sameVnode,当节点相同时调用 patchVNode
patchVNode中会再次调用 updateChildren 进行更新

这是一个间接的递归

有个疑问
Vue一直再调用 updateChildren

既然是diff,应该会生成一个差异补丁
但 updateChildren 并未有返回值,那么patch之后最终如何渲染到UI上的?


Vue在模板编译完成之后会生成组件的渲染函数

这个渲染函数调用之后会返回VNode节点

1
vnode = render.call(vm._renderProxy, vm.$createElement);
1
2
3
updateComponent = function () {
vm._update(vm._render(), hydrating);
};

vm_render() 调用render function 获得VNode,然后进行_update,根据VNode将DOM渲染到HTML中

也即,render 获得 VDOM_update 根据 VDOM 来渲染 DOM


如果没记错的话 target stack 是 Vue2 中才引入的机制,而 Vue1 中则是仅靠 Dep.target 来进行依赖收集的。根据我自己对 Vue1 和 Vue2 差异的理解,引入 target stack 的原因在于 Vue2 使用了新的视图更新方式。

具体来说,vue1 视图更新采用的是细粒度绑定的方式,而 vue2 采取的是 virtual DOM 的方式。举个例子来说可能比较容易理解,对于下面的模版:

1
2
3
4
5
6
7
8
9
<!-- root -->
<div>
{{ a }}
<my :text="b"></my>
{{ c }}
<div>

<!-- component my -->
<span>{{ b }}</span>

Vue1 的处理方式可以简化理解为:

1
2
3
watch(for a) -> directive(update {{ a }})
watch(for b) -> directive(update {{ b }})
watch(for c) -> directive(update {{ c }})

由于是数据到 DOM 操作操作指令的细粒度绑定,所以不论是指令还是 watcher 都是原子化的。对于上面的模版,在处理完 的视图绑定后,创建新的 vue 实例 my 并且处理 的视图绑定,随后继续处理 的绑定。

而在 Vue2 中情况就完全不同,视图被抽象为一个 render 函数,一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:

1
2
3
4
5
renderRoot () {
...
renderMy ()
...
}

可以看到在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,此时就需要中断 root 而进行 my 的 evaluate,当 my 的 evaluate 结束后 root 将会继续进行,这就是 target stack 的意义。

未完待续。。