Vue的Diff算法

Diff算法

新旧虚拟DOM对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较。 所以Diff算法是:深度优先算法。 时间复杂度:O(n)

如何触发

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图。

Vue触发Diff的流程

核心方法:

1. patch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function patch(oldVnode, newVnode) {
// 比较是否为一个类型的节点
if (sameVnode(oldVnode, newVnode)) { **// 重点**
// 是:继续进行深层比较
patchVnode(oldVnode, newVnode) **// 重点**
} else {
// 否
const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
const parentEle = api.parentNode(oldEl) // 获取父节点
createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
// 设置null,释放内存
oldVnode = null
}
}
return newVnode
}

这个方法作用就是,对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准,下面会讲)

  • 是:继续执行patchVnode方法进行深层比对
  • 否:没必要比对了,直接整个节点替换成新虚拟节点

2. sameVnode方法

1
2
3
4
5
6
7
8
9
function sameVnode(oldVnode, newVnode) {
return (
oldVnode.key === newVnode.key && // key值是否一样
oldVnode.tagName === newVnode.tagName && // 标签名是否一样
oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
)
}

patch关键的一步就是sameVnode方法判断是否为同一类型节点

3. patchVnode方法

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
function patchVnode(oldVnode, newVnode) {
const el = newVnode.el = oldVnode.el // 获取真实DOM对象
// 获取新旧虚拟节点的子节点数组
const oldCh = oldVnode.children, newCh = newVnode.children
// 如果新旧虚拟节点是同一个对象,则终止
if (oldVnode === newVnode) return
// 如果新旧虚拟节点是**文本节点**,且文本不一样
if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
// 则直接将真实DOM中文本更新为新虚拟节点的文本
api.setTextContent(el, newVnode.text)
} else {
// 否则

if (oldCh && newCh && oldCh !== newCh) {
// 新旧虚拟节点都有子节点,且子节点不一样

// 对比子节点,并更新
updateChildren(el, oldCh, newCh) **// 重点**
} else if (newCh) {
// 新虚拟节点有子节点,旧虚拟节点没有

// 创建新虚拟节点的子节点,并更新到真实DOM上去
createEle(newVnode)
} else if (oldCh) {
// 旧虚拟节点有子节点,新虚拟节点没有

//直接删除真实DOM里对应的子节点
api.removeChild(el)
}
}
}

这个函数做了以下事情:

  • 找到对应的真实DOM,称为el
  • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为newVnode的文本节点。
  • 如果oldVnode有子节点而newVnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要

4. updateChildren方法

这是patchVnode里最重要的一个方法,新旧虚拟节点的子节点对比,就是发生在updateChildren方法

是怎么样一个对比方法呢?就是首尾指针法,新的子节点集合和旧的子节点集合,各有首尾两个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>

修改数据后

<ul>
<li>b</li>
<li>c</li>
<li>e</li>
<li>a</li>
</ul>

那么新旧两个子节点集合以及其首尾指针为:

Untitled

然后会进行互相进行比较,总共有五种比较情况:

  • 1、oldS 和 newS 使用sameVnode方法进行比较,sameVnode(oldS, newS)
  • 2、oldS 和 newE 使用sameVnode方法进行比较,sameVnode(oldS, newE)
  • 3、oldE 和 newS 使用sameVnode方法进行比较,sameVnode(oldE, newS)
  • 4、oldE 和 newE 使用sameVnode方法进行比较,sameVnode(oldE, newE)
  • 5、如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnode 的 key 去找出在旧节点中可以复用的位置。

Untitled

分析过程:

Untitled

  • 第一步
1
2
oldS = a, oldE = c
newS = b, newE = a

比较结果:oldS 和 newE相等,需要把节点a移动到newE所对应的位置,也就是末尾,同时oldS++newE--

Untitled

  • 第二步
1
2
oldS = b, oldE = c
newS = b, newE = e

比较结果:oldS 和 newS相等,需要把节点b移动到newS所对应的位置,同时oldS++,newS++

Untitled

  • 第三步
1
2
oldS = c, oldE = c
newS = c, newE = e

比较结果:oldS、oldE 和 newS相等,需要把节点c移动到newS所对应的位置,同时oldS++,newS++

Untitled

  • 第四步

oldS > oldE,则oldCh先遍历完成了,而newCh还没遍历完,说明newCh比oldCh多,所以需要将多出来的节点,插入到真实DOM上对应的位置上

Untitled

updateChildren的核心原理代码

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
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0, 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
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx] //先判断节点是否为null
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
} else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}

参考来源

15张图,20分钟吃透Diff算法核心原理,我说的!!! - 掘金


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!