background
Review Vue2
the double-ended comparison method~
double-ended comparison
The double-ended comparison method adopted by Vue2, that is, the head-to-tail comparison between the new list and the old list. During the comparison process, the pointer will gradually move inward until all the nodes of a certain list have been traversed, and the comparison stops.
for example:
# 旧列表
a b c d
# 新列表
d b a c
We will first define a few variables:
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];
}
Then at this time we have four pointers and the nodes corresponding to the four pointers, which are node a of the old list, node d of the old list, node d of the new list, and node c of the new list. For the convenience of the following expression: the node a of the old list will be written as, and so on oldNodeA
.
oldStartNode
andnewStartNode
contrastkey
value ;oldEndNode
andnewEndNode
contrastkey
values ;oldStartNode
andnewEndNode
contrastkey
values ;newStartNode
andoldEndNode
comparisonkey
value .
as the picture shows:
Comparison process
Next, enter our comparison process~ The comparison process is mainly to find nodes with the same key value
Achieving two-ended comparisons
Let me first talk about the double-ended comparison, if you encounter the same node (passed key
):
oldStartNode
WhennewStartNode
thekey
sum is the same, the sumoldStartIndex
movesnewStartIndex
backward one bit at the same time;oldEndNode
WhennewEndNode
thekey
sum is the same, the sum moves forward one bit at the same timeoldEndIndex
;newEndIndex
oldStartNode
WhennewEndNode
thekey
sum is the same,oldStartIndex
move one bit backward andnewEndIndex
one bit forward;oldEndNode
WhennewStartNode
thekey
sum is the same,oldEndIndex
move forward one bit andnewStartIndex
move backward one bit.
Conditions for ending the loop When all the nodes in one of the lists have been traversed, our comparison process is completed.
Take this as an example:
# 旧列表
a b c d
# 新列表
d b a c
When comparing for the first time:
oldStartNode A
Different fromnewStartNode D
, continue to compare;oldEndNode D
Different fromnewEndNode C
, continue to compare;oldEndNode D
isnewStartNode D
the same as , then the position of node D in the old list will be changed at this time, moved to the front of A, thenoldEndIndex
moved forward andnewStartIndex
backward.
The following figure is obtained:
the result of the virtual DOM at this time is:
d a b c
Next, continue to compare:
oldStartNode A
Different fromnewStartNode B
, continue to compare;- -
oldEndNode C
isnewEndNode C
the same as and, but they all belong to the tail node, so we directly reuse the node and moveoldEndIndex
both andnewEndIndex
forward
to get the following figure:
At this time, the result of the virtual DOM is:
d a b c
Next, continue to compare:
oldStartNode A
Different fromnewStartNode B
, continue to compare;oldEndNode B
Different fromnewEndNode A
, continue to compare;oldStartNode A
AndnewEndNode A
, just move the A node in the old list behind the B node,oldStartIndex
move one bit backward, andnewEndIndex
move one bit forward
to get the following picture:
At this time, the result of the virtual DOM is:
d b a c
Finally, it is consistent oldEndNode B
with newEndNode B
the comparison, and then ends the cycle ~
the general code is as follows:
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 {
// ...
}
}
}
The above is the case where the same node will appear in the comparison of the old and new nodes at the head and tail. Next, let's look at the situation where no reuse node can be found in the four comparisons:
oldStartNode A
newStartNode E
different fromoldEndNode D
newEndNode H
different fromoldStartNode A
newEndNode H
different fromoldEndNode
D isnewStartNode E
different from;
At this time, we will first take out the first node of the new list , and then find out whether there is a node that can be reused in the old list. There are two situations here. Let’s talk about the one on the picture first: if oldStartNode A
it does not exist, then it means that this is a new node, and it can be placed directly at the front of the old list ;
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]
}
}
}
The problem that needs to be considered at this time is that when entering the next cycle, the corresponding oldStartNode is undefined, so the code needs to be dealt with:
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 {
// ...
}
}
}
Next, let's consider the case of list deletion:
- Node A multiplexing;
- Node D is multiplexed;
at this time, each coordinate changes as follows:
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
if (newStartIndex > newEndIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
// 删除节点
parent.removeChild(prevChildren[oldStartIndex++].el)
}
}
Finally, it is the case of adding nodes~
- Node A multiplexing;
- Node B is multiplexed;
at this time, each coordinate changes as follows:
if (oldStartIndex> oldEndIndex ) {
// 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
}
Finally, paste the complete code:
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)
}
}
}
summary
Re-learned Vue2's diff algorithm: double-ended comparison method, with new gains, the thinking is much clearer. I will write a Vue3 diff algorithm later~