Vue虚拟dom与Diff算法
# Vue 虚拟 DOM 和 Diff 算法
起初我们在使用 JS/JQuery 时,不可避免的会大量操作 DOM,而 DOM 的变化又会引发回流或重绘,从而降低页面渲染性能。
为了 减少频繁操作 DOM 而引起回流重绘所引发的性能问题
,虚拟 DOM 应运而生。
虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力。因为很多人是在学习 React 或者 Vue 的过程中接触到的虚拟 DOM ,所以为先入为主,认为虚拟 DOM 和 JSX 密不可分。其实不然,Vue 即使只使用模版,也能把虚拟 DOM 玩得风生水起。 当然同时也有很多人通过 babel 在 Vue 中使用 JSX。
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。
虚拟 DOM 最大的优势在于抽象了原本的渲染过程,为函数式的 UI 编程方式打开了大门
, 实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是很火热的小程序,也可以是各种软件的 GUI。
# 虚拟 DOM 的作用是什么?
- 虚拟 DOM 最大的优势在于抽象了原本的渲染过程,为函数式的 UI 编程方式打开了大门, 实现了跨平台的能力,而不仅仅局限于浏览器的 DOM
- 兼容性好。因为 Vnode 本质是 JS 对象,所以不管 Node 还是浏览器环境,都可以操作
- 真实 DOM 在频繁操作时引发的回流重绘导致性能很低,虚拟 DOM 不会进行回流和重绘;虚拟 DOM 频繁修改,然后一次性对比差异并修改真实 DOM,只更新差异部分,所以进行局部回流重绘,减少了真实 DOM 中多次回流重绘引起的性能损耗
# 虚拟 DOM 对真实 DOM 的抽象过程
例:真实 dom
<div id="box">
<span class="content">xingyun</span>
</div>
2
3
上面的 HTML 转换为虚拟 DOM 如下:
{
tag: 'div',
props: {
id: 'box'
},
chidren: [
{
tag: 'span',
props: {
className: 'content'
},
chidren: [
'xingyun'
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
树形结构的 DOM 结构抽象为了树形结构的 JS 对象,之后通过 Diff 算法生成真实
过程简化如下
# DIFF算法
当数据变化时,vue 如何来更新视图的?
vue 会执行 render 函数生成两颗树,一棵新树 newVnode,一棵旧树 oldVnode,然后两棵树进行对比更新找差异就是diff
,全称difference
,在 vue 里面 diff 算法是通过 patch 函数来完成的,所以有的时候也叫patch算法
当组件创建的时候,组件所依赖的属性或者数据变化时,会运行一个函数 (下面代码中的updateComponent
),该函数会做两件事:
运行
_render
生成一颗新的虚拟 dom 树(vnode tree)运行
_update
,传入_render 生成的虚拟 dom 树的根节点,对新旧两棵树进行对比,最终完成对真实 dom 的更新
核心代码如下,跟原代码有所差异,但都差不多,是这么个意思:
// vue构造函数
function Vue() {
// ... 其他代码
var updateComponent = () => {
this._update(this._render())
}
new Watcher(updateComponent)
// ... 其他代码
}
2
3
4
5
6
7
8
9
diff过程就发生在_update
diff过程就是调用patch函数,比较新老节点,一边比较一边给真实DOM打补丁**(patch)**;
function patch(oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
function sameVnode(a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
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
sameVnode
函数用来判断两个节点是否有比较的必要,如果没有,直接替换,如果有必要执行patchVnode
那如果不是同一节点,但是它们子节点一样怎么办?敲重点:diff 是同层比较,不存在跨级比较的,也就是说 diff 为深度优先遍历!
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return // 新旧节点指向同一个对象 直接return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text) // 文本不同 更新文本
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch) // 如果都有子节点 调用updateChildren
}else if (ch){
createEle(vnode) // 如果新节点有子节点,老节点没有,直接加上子节点
}else if (oldCh){
api.removeChildren(el) // 如果新节点没有子节点,老节点有,直接删除子节点
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
由上patchVnode
函数可以发现, 最复杂的情况也就是新老节点都有子节点,那么 updateChildren 是如何来处理这一问题的,该方法也是 diff 算法的核心,下面我们来了解一下
# updateChildren
源码
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
// 新旧节点头尾四次比较 start
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
// 新旧节点头尾四次比较 end
else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: null
if (isUndef(idxInOld)) {
// 旧节点没有这个key 证明是新节点 直接新增节点
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm
)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (sameVnode(elmToMove, newStartVnode)) {
// 旧节点有这个key,并且节点相同 直接移动到对应位置
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
newStartVnode.elm,
oldStartVnode.elm
)
newStartVnode = newCh[++newStartIdx]
} else {
// 旧节点有这个key,但是节点不同 当成新节点处理 直接新增节点
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm
)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
// 老节点先遍历完成 将剩余的新节点添加到最后
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
)
} else if (newStartIdx > newEndIdx) {
// 新节点先遍历完成 删除剩余的老节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
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
105
106
107
108
109
110
111
112
113
114
115
代码中对一些关键位置做了注释
再用图来解析一下这个过程
第一步、指针处相互比较,即有四种对比,分别为:a 与 b、千与 g、a 与 g、千与 b,没有相等的继续
第二步、此处分为俩种情况,有 key 和无 key,无 key 的情况直接创建新的 Node 插入到 oldStartldx 指针对应的 Node (a)之前;有 key 的情况:取 newStartldx 指针处的值,到 old VNode 里面找,发现了有相同的 b,记录此时的 oldkeyToldx,随即调整真实 DOM
Node, 将 DOM Node 中的 b 移动到 oldStartidx 指针对应的 Node (a)之前。如下图
然后找到 old VNode 中的 oldkeyToldx 对应的值设置为 undefined, newStartidx 指针往中
问靠拢,newStartldx++,此时 old VNode 和 new VNode 指针如下
判断 oldStartldx <= oldEndldx && newStartldx <= newEndldx,结果为真,继续循环流程
此时四种对比为:a 与 f a 与 g g 与 f f (old VNode)与 f (new VNode)
最后一个对比结果为真,调整真实 DOM Node,将 DOM Node 中的千移动到 oldStartldx 指针对应的 Node (a)之前,如下图
然后 newStartldx 指针以及 oldEndldx 指针往中间靠拢,newStartldx++, oldEndildx-
判断 oldStartldx <= oldEndldx && newStartildx <= newEndldx,结果为假,跳出循
环,判断此时的指针:
1、如果 oldStartldx > oldEndldx,说明老节点遍历完成了,新的节点比较多,所以多出
来的这些新节点,需要创建出来并添加到真实 DOM Node 后面。
2、如果 newStartldx > newEndldx,说明新节点遍历完成了,老的节点比较多,所以多
出来的这些老节点,需要从真实 DOM Node 中删除这些节点.
此时我们符合场景二,所以需要从真实 DOM Node 中删除 [oldStartldx,oldEndldx]区问
中的 Node 节点,删除过程中,对 node 节点判断,为 false 直接跳过,为 true 直接删除
即删除掉真实 DOM Node 中的 a、c、d、e 四个节点,如下图解
Diff 过程完成