vue学习笔记-解析diff算法
在vue中,DOM被抽象为一个以JavaScript对象为节点的virtual DOM(虚拟DOM)树,当数据发生改变时,会通过修改virtual DOM,使用diff算法,修改局部的内容,然后将这部分内容以打补丁的方式来修改真实的DOM,来达到视图修改的作用。
因为通过diff算法,能得出需要修改的最小单位,所以我们在更新视图的时候,只需要修改这些最小单位,这么做减小了更新的消耗。
diff算法
diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,时间复杂度只有O(n)
diff算法流程
diff算法通过比较该层的树节点,当不同的时候,根据不同情况替换节点,当相同的时候判断新的虚拟树和原虚拟树是否都有子节点,有的话进入下一层进行比较
图片来自:详解vue的diff算法
patch
patch将新老节点进行比较,每次比较同一层的节点,patch的核心就是diff算法,通过diff算法来高效地比较新老虚拟树的差异,得出变化并修改视图
图片来自:VirtualDOM与diff(Vue实现)
这里相同颜色框里的节点就是比较的节点
patch的代码如下,这里要把所有部分看的一清二楚有点困难,根据上面的流程图,我们看到,要进入patchVnode的话,必须是传入的oldVnode确定为节点且与新的vnode通过sameVnode比较时返回true才可,所以下面看看sameVnode
1 |
|
sameVnode
怎么才算节点不同,diff算法中使用sameVnode来进行比较,当比较相同时才做patchVnode处理
1 |
|
patchVnode
patchVnode对于节点的处理
判断新老Vnode是否都为静态且key相同,并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可;
判断新老节点是否都有子节点,如果都有的且子节点不同,就对子节点使用diff操作,更新子节点
如果新节点有子节点,老节点没有的话,就把老节点里面的文本内容清空,然后将新的子节点插入
如果老节点有子节点,新节点没有的话,就移除这个子节点
当新老节点都没有文本的时候,就直接替换文本
代码如下: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/*patch VNode节点*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
/*两个VNode节点相同则直接返回*/
if (oldVnode === vnode) {
return
}
/*
如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
那么只需要替换elm以及componentInstance即可。
*/
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
/*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/
i(oldVnode, vnode)
}
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
/*调用update回调以及update钩子*/
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)
}
/*如果这个VNode节点没有text文本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
/*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
/*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
/*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
/*当新老节点text不一样时,直接替换这段文本*/
nodeOps.setTextContent(elm, vnode.text)
}
/*调用postpatch钩子*/
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}updateChildren
updateChildren是diff算法中具体对节点的操作,通过sameVnode来判断是否要进行操作,对不同节点的判断结果决定使用什么操作
(下面的图片均来自VirtualDOM与diff(Vue实现))
updateChildren方法在一开始首先在old VNode和new VNode首尾共设置了四个指针,通过比较这四个指针的值和new VNode与old VNode中的指针来进行不同的操作,总的有六种情况
(为了方便,下面将oldStartIdx称为oS,oldEndIdx称为oE,下面两个指针称为nS和nE)比较情况
比较oS和nS
比较的时候都是使用sameVnode来进行比较,如果返回true的话,就对这两个指针指向的节点使用patchVnode操作,然后两个指针都向后,如图:比较oE和nE
oE和nE的比较与第一种情况类似,对sameVnode返回true的两个节点使用patchVnode操作,然后两个指针都向前移动比较oS和nE
oS和nE比较,如果sameVnode返回true,那么将当前oS指向的节点,移动到当前oE指向的节点所在的位置的后面,同时对该节点和nE指向的节点使用patchVnode操作,oS的指针向后移动,nE的指针向前移动比较oE和nS
当oE和nS使用sameVnode返回true时,将oE指向的节点放到oS指向的节点的前面,对该节点和nS指向的节点使用patchVnode操作,然后oE指针向前移动,nS指针向后移动nS和oS到oE之间的节点比较(为true)
当上面的四种情况通过sameVnode都没法返回true的时候,则会创建一个key为旧的vNode,value为vNode对应index的哈希表,然后使用nS指向的vNode和哈希表中的vNode比较,为true的时候,将这个vNode(图中的elmToMove指针指向的vNode)放到oS指向的vNode的前面,同时对该vNode和nS指向的vNode进行patchVnode操作,然后nS指针向右移动nS和oS到oE之间的节点比较(为false)
同上面第5的情况,但是我们在oS和oN之间也找不到和nS指向的vNode相同的vNode,所以此时我们要调用createElm来新建一个vNode,并将其插入到oS指向的vNode之前的位置,并对其和nS指向的vNode使用patchVnode操作,随后nS指针向右移动结束情况
结束有两种情况
oS>oE
当oS>oE,即是说老的节点比新的节点少,所以接下来就要将剩余的节点插入,插入的位置在oS后面nS>nE
如果nS>nE,那么说明新节点已经遍历完了,而老节点还没遍历完,那么还没遍历到的老节点,也就是新节点中没有的,对于这部分节点,我们可以直接丢弃掉
下面是代码的实现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
91function 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]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
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]
} else {
/*
生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2
结果生成{key0: 0, key1: 1, key2: 2}
*/
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
/*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
/*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
/*获取同key的老节点*/
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
/*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
/*Github:https://github.com/answershuto*/
/*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
/*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
oldCh[idxInOld] = undefined
/*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
/*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}