Virtual Dom
vue2.0从引入了Virtual dom的概念作为优化dom操作的手段, 下面我们就详细介绍这玩意儿。
中所周知, VNode是virtual dom的主要角色, 先看看VNode的组成
VNode
1 | class VNode { |
以上是VNode的结构 这比真实dom少了很多属性 所以在通过比对VDom的变化在映射到真实dom的过程中会节省很多性能开销
Patch之前
本章主要讲vue在数据更新时是怎样及时的通过对比新旧virtual dom映射到真实dom的~
从源码来看, 有三个类极为重要 Observer类、Dep类和Watcher类
Observer实例负责给每个需要watch的对象添加getter和setter
Watcher实例则负责在数据变动的时候去收集依赖、触发patch等操作
而Dep实例则是observer和watch的桥梁 在数据变动的时候通知对应watcher执行更新操作
那么在数据更新的过程中都发生了什么?
observer.get -> dep.notify -> watcher.update -> flushSchedulerQueue ->
watcher.run -> watch.get -> updateComponent -> _render -> _update -> patch
-> patchVnode -> updateChildren
本章主要从patch的过程开始讲
Patch
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
103function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var 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 {
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
var oldElm = oldVnode.elm;
var 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)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.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
}
- 如果vnode不存在儿oldVnode存在 销毁oldVnode
- 如果oldVnode不存在而vnode存在 则调用createElm创建新的节点
- 如果oldVnode存在
- 如果oldVnode不是真实节点并且oldVnode和vnode是相同节点 则调用patchVnode进行比较
- 如果oldVnode是真实节点(说明是组件初始化的过程), 调用createElm创建新节点 并调用removeVnodes将oldVnode对应的老节点移除
patchVnode
1 | function patchVnode ( |
- 如果oldVnode完全等于vnode return
- 如果oldVnode和vnode都是静态节点,且vnode的key等于oldVnode的key,且vnode是克隆节点或者v-once控制的节点的时候 只需要把
oldVnode的componentInstance复制给vnode即可 - 如果vnode不是文本节点
- 如果oldVnode存在children且vnode存在children 且两个children 不完全相等 执行updateChildren
- 如果只有vnode存在children子节点, 调用addVnodes添加子节点
- 如果只有oldVnode存在子节点, 调用removeVnodes移除这些子节点
- 如果vnode和oldVnode都没有子节点,但是oldVnode是文本节点 则把oldVnode对应的真实dom的文本内容清空
- 如果vnode是文本节点且文本内容和oldnode的文本内容不一样, 则把vnode的文本内容赋值给oldVnode对应dom节点的文本
updateChildren
1 | function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
updateChildren方法主要通过while循环去对比新旧两棵树的子节点来更新dom
- 如果oldStartVnode不存在 则将oldStartVnode设置为下一个子节点 oldStartIdx也指向这个子节点
- 如果oldEndVnode不存在 oldEndVnode指向上一个子节点 oldEndIdx也指向这个子节点
- 如果oldStartVnode和newStartVnode是同一个子节点 调用patchVnode 进行比较 并且将oldStartVnode和newStartVnode设置为下一个子节点
- 如果oldEndVnode和newEndVnode是同一个节点, 调用patchVnode进行比对, 同时将他们设置为上一个节点
- 如果oldStartVnode和newEndVnode是同一个子节点, 调用patchVnode进行比对 将这个节点移到oldEndVnode下一个兄弟节点的前面 并将oldStartVnode指向下一个 newEndVnode指向下一个
- 如果oldEndVnode和newStartVnode是同一个子节点 调用patchVnode进行比对 将这个节点移到oldStartVnode对应节点的前面 并将oldEndVnode指向前一个节点 newStartVnode指向下一个节点
- 如果以上都不成立 根据newStartVnode的key值在oldVnodeChildren中进行查找
- 如果oldVnodeChildren中存在key值一样
- 如果oldVnodeChildren中找到的节点和newStartVnode是相同节点,调用patchVnode的比对并将目标节点移至oldStartVnode的前面
- 如果不是相同节点 则调用createElm创建新的节点
- 如果找不到和newStartVnode的key值一样的节点 则调用createElm创建新的节点
跳出while循环后 还要针对oldStartIdx和oldEndIdx进行操作
- 如果oldStartIdx 大于 oldEndIdx 说明newVnodesChildren没有遍历完 执行addVnodes
- 如果newStartIdx > newEndIdx 说明oldVnodeChildren没有遍历完 执行removeVnodes
以上就是整个patch过程, 尤其是updateChildren的逻辑还是很精妙的 通过不断缩减两条边界线去判断有无相同节点 不管是空间复杂度还是时间复杂度都减少了很多