背景
最近复习的过程中,准备对比一下Vue2和Vue3的diff算法区别,好知道两者直接的差异和优缺点。刚好看了网上的文章,但是对方写的代码不太正确,所以特意记录一下我的学习过程~
双端比较法
Vue2采用的双端比较法,即新列表和旧列表进行头尾对比,在对比的过程中指针会逐渐向内靠拢,直到某个列表的节点全部遍历过,对比停止。
举个例子:
# 旧列表
a b c d
# 新列表
d b a c
我们会先定义几个变量:
function vue2diff(prevChildren, nextChildren, parent) {
let
// 旧头指针
oldStartIndex = 0,
// 新头指针
newStartIndex = 0,
// 旧尾指针
oldEndIndex = prevChildren.length - 1,
// 新尾指针
newEndIndex = nextChildren.length - 1,
// 旧头节点
oldStartNode = prevChildren[oldStartIndex],
// 旧尾节点
oldEndNode = prevChildren[oldEndIndex],
// 新头节点
newStartNode = nextChildren[newEndIndex],
// 新尾节点
newEndNode = nextChildren[newStartIndex];
}
那么此时我们就有四个指针和四个指针对应的节点了,分别是旧列表的节点a、旧列表的节点d、新列表的节点d和新列表的节点c,为了下面方便表述:旧列表的节点a会写为oldNodeA
,以此类推~
接下来说一下双端比较法的比较方法:
oldStartNode
和newStartNode
对比key
值;oldEndNode
和newEndNode
对比key
值;oldStartNode
和newEndNode
对比key
值;newStartNode
和oldEndNode
对比key
值。
如图所示:
对比流程
接下来进入我们的对比流程~对比的过程主要是寻找拥有相同key值的节点
实现双端对比
先说一下双端对比中,如果遇到相同节点(拥有通过key
)的情况:
oldStartNode
和newStartNode
的key
相同时,则oldStartIndex
和newStartIndex
同时向后移动一位;oldEndNode
和newEndNode
的key
相同时,则oldEndIndex
和newEndIndex
同时向前移动一位;oldStartNode
和newEndNode
的key
相同时,则oldStartIndex
向后移动一位,newEndIndex
向前移动一位;oldEndNode
和newStartNode
的key
相同时,则oldEndIndex
向前移动一位,newStartIndex
向后移动一位。
结束循环的条件当其中一个列表的节点全部遍历完成时,则完成我们的对比过程。
按这个为例子:
# 旧列表
a b c d
# 新列表
d b a c
第一次对比的时:
oldStartNode A
与newStartNode D
不同,继续对比;oldEndNode D
与newEndNode C
不同,继续对比;oldEndNode D
与newStartNode D
相同,则这时候会将旧列表中得到节点D的位置进行变化,挪到A的前面,然后oldEndIndex
向前移动,newStartIndex
向后移动。
得到如下图:
此时虚拟DOM的结果为:
d a b c
接下来继续对比:
oldStartNode A
与newStartNode B
不同,继续对比;- -
oldEndNode C
与newEndNode C
相同,但是它们都属于尾节点,所以我们直接复用节点,将oldEndIndex
和newEndIndex
都向前移动
得到如下图:
此时虚拟DOM的结果为:
d a b c
接下来继续对比:
oldStartNode A
与newStartNode B
不同,继续对比;oldEndNode B
与newEndNode A
不同,继续对比;oldStartNode A
与newEndNode A
,则将旧列表中A节点挪到B节点后面即可,oldStartIndex
向后移动一位,newEndIndex
向前移动一位
得到如下图:
此时虚拟DOM的结果为:
d b a c
最后就是oldEndNode B
与newEndNode B
对比,一致,然后结束了循环~
大致代码如下:
function vue2diff(prevChildren, nextChildren, parent) {
// ...
// 双端对比,当有一个列表的节点全部遍历完成,则结束循环
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
if (oldStartNode.key === newStartNode.key) {
// 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
// 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
// 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
// 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
} else {
// ...
}
}
}
以上是四个头尾新旧节点对比会出现相同节点的情况,接下来我们来看一下四次对比找不到复用节点的情况:
oldStartNode A
与newStartNode E
不同;oldEndNode D
与newEndNode H
不同;oldStartNode A
与newEndNode H
不同;oldEndNode
D 与newStartNode E
不同;
这时候我们会先拿出新列表的第一个节点oldStartNode A
,然后找到旧列表中是否存在可以复用的节点,这里就有两种情况啦,先说图上的这种:如果不存在,那么说明这个是新节点,直接放在旧列表的最前面即可;
如果是这种:
B的复用节点在旧列表中,那么将旧节点移动到第一个节点,并在旧列表中置成undefine
function vue2diff(prevChildren, nextChildren, parent) {
// ...
// 双端对比,当有一个列表的节点全部遍历完成,则结束循环
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
if (oldStartNode.key === newStartNode.key) {
// 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
// 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
// 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
// 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
} else {
// 四个节点对比的过程中没有发现相同的节点时
// 先看看新列表的头节点是否存在于旧列表中
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
// 如果不存在,直接创建放在最前方即可
if (oldIndex === -1) {
mount(newStartNode, parent, oldStartNode.el)
} else {
// 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
let prevNode = prevChildren[oldIndex]
patch(prevNode, newStartNode, parent)
parent.insertBefore(prevNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
// 更新新列表的对比节点
newStartIndex++
newStartNode = nextChildren[newStartIndex]
}
}
}
这时候需要考虑的问题是,当进入下一个循环的时候,对应的oldStartNode为undefined,所以代码上需要处理一下:
function vue2diff(prevChildren, nextChildren, parent) {
let
// 旧头指针
oldStartIndex = 0,
// 新头指针
newStartIndex = 0,
// 旧尾指针
oldEndIndex = prevChildren.length - 1,
// 新尾指针
newEndIndex = nextChildren.length - 1,
// 旧头节点
oldStartNode = prevChildren[oldStartIndex],
// 旧尾节点
oldEndNode = prevChildren[oldEndIndex],
// 新头节点
newStartNode = nextChildren[newEndIndex],
// 新尾节点
newEndNode = nextChildren[newStartIndex];
// 双端对比,当有一个列表的节点全部遍历完成,则结束循环
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
// 旧列表中遇到Undefine节点则跳过对比
if (oldStartNode === undefined) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefined) {
oldEndNode = prevChildren[--oldStartIndex]
} else if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
// ...
} else if (oldEndNode.key === newStartNode.key) {
// ...
} else {
// ...
}
}
}
接下来我们来考虑一下列表删除的情况:
- 节点A复用;
- 节点D复用;
此时各个坐标变化如下:
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
if (newStartIndex > newEndIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
// 删除节点
parent.removeChild(prevChildren[oldStartIndex++].el)
}
}
最后是新增节点的情况啦~
- 节点A复用;
- 节点B复用;
此时各个坐标变化如下:
if (oldStartIndex> oldEndIndex ) {
// 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
}
最后贴一下完整代码:
function vue2diff(prevChildren, nextChildren, parent) {
let
// 旧头指针
oldStartIndex = 0,
// 新头指针
newStartIndex = 0,
// 旧尾指针
oldEndIndex = prevChildren.length - 1,
// 新尾指针
newEndIndex = nextChildren.length - 1,
// 旧头节点
oldStartNode = prevChildren[oldStartIndex],
// 旧尾节点
oldEndNode = prevChildren[oldEndIndex],
// 新头节点
newStartNode = nextChildren[newEndIndex],
// 新尾节点
newEndNode = nextChildren[newStartIndex];
// 双端对比,当有一个列表的节点全部遍历完成,则结束循环
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
// 旧列表中遇到Undefine节点则跳过对比
if (oldStartNode === undefined) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefined) {
oldEndNode = prevChildren[--oldStartIndex]
} else if (oldStartNode.key === newStartNode.key) {
// 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
// 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
// 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
// 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
} else {
// 四个节点对比的过程中没有发现相同的节点时
// 先看看新列表的头节点是否存在于旧列表中
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
// 如果不存在,直接创建放在最前方即可
if (oldIndex === -1) {
mount(newStartNode, parent, oldStartNode.el)
} else {
// 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
let prevNode = prevChildren[oldIndex]
patch(prevNode, newStartNode, parent)
parent.insertBefore(prevNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
// 更新新列表的对比节点
newStartIndex++
newStartNode = nextChildren[newStartIndex]
}
}
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
if (newStartIndex > newEndIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
parent.removeChild(prevChildren[oldStartIndex++].el)
}
} else if (oldStartIndex> oldEndIndex ) {
// 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
}
}
小结
重新学了一遍Vue2的diff算法:双端比较法,有了新的收获吧,思路清晰了很多。后面会写一篇Vue3的diff算法~
参考链接
- 【Vue源码】第十八节DOM更新以及diff算法
- React、Vue2、Vue3的三种Diff算法