辅助方法
1 | // 更新数据 |
React
算法流程
- 遍历
next
取出nextNode
,位置i
- 在
prev
中查找nextNode.key
一致的节点,如果没找到,则创建后插入到next[i - 1]
之后 - 如果找到节点,位置为
j
,更新节点,判断j
是否小于lastIdx(lastIdx = 0)
,如果不小于,则lastIdx = j
- 如果小于
lastIdx
则将节点插入到next[i - 1]
之后 - 遍历
next
结束后,遍历prev
如果节点不在next
中,则删除节点
代码实现
1 | // react |
执行流程
第 1 行表示模拟 prev 数组 (一份拷贝),点状表示移动过的节点,虚线表示更新过的节点
第 2 行表示旧的 prev 数组,绿色表示操作过,红色表示当前遍历的值
第 3 行表示新的 next 数组
按照 react
diff
的流程来模拟下步骤
遍历 prev
生成一份 key
与 index
的映射表 prevIndex
遍历 next
数组,i = 0
lastIdx = 0
通过 prevIndex[key]
得出 j = 1
j
大于 lastIdx
,lastIdx = j = 1
只执行更新操作不执行移动
i = 1
lastIdx = 1
通过 prevIndex[key]
得出 j = 0
j
小于 lastIdx
所以执行更新操作和移动操作,需要把 a
移动到 next[i - 1].next
也就是 c
节点之前
i = 2
lastIdx = 1
通过 prevIndex[key]
得出 j = 3
j
大于 lastIdx
,lastIdx = j = 3
执行更新操作
i = 3
lastIdx = 3
通过 prevIndex[key]
得出 j = 2
j
小于 lastIdx
所以执行更新操作和移动操作,需要把 c
移动到 next[i - 1].next
也就是 f
节点之前
i = 4
lastIdx = 3
通过 prevIndex[key]
找不到对应的节点位置,所以这里需要新增一个节点 e
,把 e
移动到 next[i - 1].next
也就是 f
节点之前
遍历完成后,我们需要重新遍历一遍 prev
然后通过 nextIndex
查找得出 f
节点已经不存在于新的节点数组之中,需要执行删除操作移除 f
节点
Vue2
算法流程
- 声明4个变量
prevStart
,prevEnd
,nextStart
,nextEnd
取出prevStartNode
,prevEndNode
,nextStartNode
,nextEndNode
- 循环,条件不满足
prevStart <= prevEnd && nextStart <= nextEnd
跳出循环 prevStartNode
或prevEndNode
是否存在,不存在则prevStart++
,prevEnd--
,回到 2prevStartNode.key == nextStartNode.key
,相等则更新节点,prevStart++ nextStart++
,回到2prevEndNode.key == nextEndNode.key
,相等则更新节点,prevEnd-- nextEnd--
, 回到2prevStartNode.key == nextEndNode.key
,相等则更新节点, 将prevStartNode
插入到prevEndNode.next
之前,prevStart++ nextEnd--
,回到 2prevEndNode.key == nextStartNode.key
,相等则更新节点,将prevEndNode
插入到prevStartNode
之前,prevEnd-- nextStart++
,回到 2- 如果全都不相等,查找
prev
,看是否存在一个与nextStartNode.key
相同的节点,有则更新,没有则创建一个新的节点,将其插入到prevStartNode
之前,nextStart++,prev[j]
标记为操作过,回到2 - 循环结束后 如果
prevEnd < prevStart
证明存在新节点未处理,从nextStart
开始 插入节点,直到newEnd
,每次节点都插入在next[newEnd + 1]
之前 - 如果
nextEnd < newStart
证明存在节点被移除,未处理,从prevStart
开始 移除节点,直到prevEnd
代码实现
1 | // vue2 |
执行流程
初始化 prevIndex
, 此时 prevStart = 0
, prevEnd = 4
, nextStart = 0
, nextEnd = 4
通过比较 prevStart
与 nextStart
,prevEnd
与 nextEnd
,prevStart
与 nextEnd
,prevEnd
与 nextStart
得知皆不想等,则通过 prevIndex
获取到 nextStartNode
是可以复用的节点,位置在 j = 1
,将 prev[j]
更新后,将其设置为 undefined
,并且将 nextStartNode
插入到 prevStartNode
之前,然后 nextStartNode = next[++nextStart]
此时 prevStart = 0
, prevEnd = 4
, nextStart = 1
, nextEnd = 4
满足 prevStartNode.key === nextStartNode.key
此时直接更新 prevStartNode
然后执行 prevStart++ nextStart++
此时 prevStartNode
是先前处理过的节点,所以直接跳过 prevStart++
此时 prevStart = 2
, prevEnd = 4
, nextStart = 2
, nextEnd = 4
通过比较 prevStart
与 nextStart
,prevEnd
与 nextEnd
,prevStart
与 nextEnd
,prevEnd
与 nextStart
得知皆不想等,则通过 prevIndex
获取到 nextStartNode
是可以复用的节点,位置在 j = 3
,将 prev[j]
更新后,将其设置为 undefined
,并且将 nextStartNode
插入到 prevStartNode
之前,然后 nextStartNode = next[++nextStart]
此时 prevStart = 2
, prevEnd = 4
, nextStart = 3
, nextEnd = 4
满足 prevStartNode.key === nextStartNode.key
此时直接更新 prevStartNode
然后执行 prevStart++ nextStart++
此时 prevStartNode
是先前处理过的节点,所以直接跳过 prevStart++
此时 prevStart = 4
, prevEnd = 4
, nextStart = 4
, nextEnd = 4
通过比较 prevStart
与 nextStart
,prevEnd
与 nextEnd
,prevStart
与 nextEnd
,prevEnd
与 nextStart
得知皆不想等,且在 prevIndex
中也没法获取到此节点,证明这个为新增节点,创建节点后插入到 prevNodeStart
之前,执行 nextStartNode = next[++nextStart]
此时 prevStart = 4
, prevEnd = 4
, nextStart = 5
, nextEnd = 4
,此时已经不满足循环条件了,所以直接跳出
由判断 prevStart > prevEnd
不成立,但 nextStart > nextEnd
成立,所以存在节点是未删除的,循环删除掉多余的节点,条件为 prevStart <= prevEnd
最后得到
在看一组巩固
此时 prevStart = 0
, prevEnd = 3
, nextStart = 0
, nextEnd = 4
通过比较,得知 prevStartNode
与 nextStartNode
相等,更新后移动指针位置
通过比较,得知 prevEndNode
与 nextEndNode
相等,更新节点,然后把节点移动到 prevStartNode
之前
此时 prevStart = 1
, prevEnd = 2
, nextStart = 2
, nextEnd = 4
我们发现 prevEndNode
跟 nextStartNode
是相等的,更新节点后移动指针
此时 prevStart = 1
, prevEnd = 1
, nextStart = 2
, nextEnd = 3
我们发现 prevStartNode
跟 nextStartNode
是相等的,更新节点后移动指针
此时 prevStart = 2
, prevEnd = 1
, nextStart = 3
, nextEnd = 3
已经不符合循环条件,跳出循环,此时满足 prevEnd < prevStart
,证明还有新增节点,循环将新节点插入在 next[nextEnd + 1]
之前
Vue3
算法流程
j
表示当前匹配到第几个位置,初始值为0- 从
j
开始匹配相同的元素,如果prev[j].key == next[j].key
,更新后j++
如果j > prevEnd || j > nextEnd
提前跳出 - 匹配完前面的相同元素后 j 停在第一个不同点
- 从
prevEnd
和nextEnd
开始从后面查找相同的后缀,更新后prevEnd-- nextEnd--
如果j > prevEnd || j > nextEnd
提前跳出 - 此时已经更新完相同的前缀和后缀,需要看 j 处于什么位置
- 如果
j > prevEnd && j <= nextEnd
,则next
在[j , nextEnd]
新增元素,直接在next[nextEnd + 1]
前插入新增元素 - 如果
j > nextEnd && j <= prevEnd
,则prev
在[j , prevEnd]
中元素被删除,直接删除多余元素 - 如果都不是上两种情况,则说明在
[j , prevEnd]
段存在乱序的节点,长度为nextLeft = nextEnd - j + 1
- 初始化一个辅助数组
source
长度为nextLeft
默认值为 -1,patched = 0
,move = false
,pos = 0
- 遍历
next
, 从j
到nextEnd
,生成一个key - i
的映射表keyIndex
- 遍历
prev
,从j
到prevEnd
- 如果
patched
大于nextLeft
,则说明相同元素从prev
中取完,后面均为待删除的元素,直接删除 k = keyIndex[prev[i]]
,如果k
不存在,证明该节点不存在于next
,直接删除next[k].key == prev[i].key
,则更新该节点,patched++
,source[k - j] = i
,如果k < pos
,则prev[i]
是需要移动的move = true
,否则pos = k
- 处理完成后判断
move
是否为真 - 如果不为真,
i = nextLeft - 1
倒序遍历,如果source[i] == -1
时,pos = j + i
,在next[pos + 1]
前增加节点next[pos]
- 如果为真,则在这段范围内发生乱序 / 新增的情况
- 求出最长上升子序列
seq = lis(source) k = seq.length - 1
i = nextLeft - 1
倒序遍历,如果source[i] == -1
时,pos = j + i
,在next[pos + 1]
前增加节点next[pos]
- 如果
i == seq[j]
,则需要移动此节点,pos = j + i
,在next[pos + 1]
前插入节点next[pos]
- 其他情况,
k--
代码实现
1 | // vue3 |
执行流程
首先比较前缀节点 j = 0
prevEnd = 3
nextEnd = 4
prevNode = prev[j]
nextNode = next[j]
满足循环条件,执行到第一个不同点,即 j = 1
prevNode = prev[j]
nextNode = next[j]
开始比对后缀节点,另 prevNode = prev[prevEnd]
nextNode = next[nextEnd]
,如果满足循环条件则开始循环
更新后缀节点直到不满足循环条件
此时 j = 1
prevEnd = 2
nextEnd = 3
,条件 j > prevEnd && j <= nextEnd
与 j > nextEnd && j <= prevEnd
没有被满足,j <= nextEnd
满足,证明 [j, nextEnd]
区间存在乱序或新增的情况
我们计算 nextLeft = nextEnd - j + 1 = 3
得出乱序区间大小为 3,source = [-1, -1, -1]
,从 j
到 nextEnd
遍历 next
数组,得到 keyIndex
,然后从 j
到 prevEnd
遍历 prev
数组 初始化 pos = 0
patched = 0
move = false
当 i = j = 1
,pos = 0``patched = 0``nextLeft = 3
且 patched < nextLeft
则查找 keyIndex
看 next
中是否存在节点下标 k = 2
,如果找到,更新节点,patched++
, 令 source[k - j] = i
则 source = [-1, 1, -1]
,判断 k < pos
,如果小于则是需要移动,move = true
, 这里明显不成立,所以令 pos = k = 2
当 i = 2
,pos = 2``patched = 1
,查找 keyIndex
得到 k = 1
,更新节点,patched++
令 source[k - j] = i
则 source = [2, 1, -1]
,因为 k < pos
所以这个节点是需要移动的,所以 move = true
因为需要移动所以需要通过lis(最长上升子序列)来确定最少移动的次数,seq = lis(source) = [2], k = seq.length - 1 = 1
然后我们从nextLeft - 1
往前遍历,直到 0
i = 2
时,通过 source[i] = -1
,-1
表明此节点我们未曾处理过,必然是一个新增的节点,所以我们需要新增这个节点到数组中,由于我们上面得出的 source
是一个相对坐标,逆推可以得到当前节点在 next
中的坐标为 pos = j + i = 3
则它需要插入在 next[pos + 1]
节点的前方
i = 1
时,由于 source[i] != -1
,则我们接着判断 i !== seq[k]
,由于不相等,这个节点是一个需要移动的节点,则我们将此节点移动到 next[pos + 1]
之前。
i = 0
是,判断都不成立,所以 k--
,接着就循环结束了,完成匹配
再来一遍执行流程加强下理解
这里直接跳过了前缀跟后缀的处理流程,也没啥好说的,此时 j = 0
prevEnd = 5
nextEnd = 5
,条件 j > prevEnd && j <= nextEnd
与 j > nextEnd && j <= prevEnd
没有被满足,j <= nextEnd
满足,证明 [j, nextEnd] = [0, 5]
区间存在乱序或新增的情况
根据上面的流程,我们求出 keyIndex
, nextLeft = nextEnd - j + 1 = 5, source = [-1, -1, -1, -1, -1]
,从 j
遍历到 prevEnd
当 i = j = 0
, patched = 0, pos = 0, move = false
,通过 keyIndex
可查找出 a
节点位于 next
中下标为 1 的位置,更新后,记录位置更新变量,patched++, pos = 1, move = false, source[k - j] = source[1] = 0
当 i = 1
,patched = 1, pos = 1, move = false
,通过 keyIndex
查找出 k = 0
由于 k < pos
所以节点 b 是需要移动的,更新后,记录位置,更新变量,patched++, pos = 1, move = true, source[k - j] = source[0] = 1
当 i = 2
,patched = 2, pos = 1, move = true
,通过 keyIndex
查找出 k = 2
由于 k > pos
所以 pos = k = 2
,更新后,记录位置,更新变量,patched++, pos = 2, source[k - j] = source[2] = 2
当 i = 3
,patched = 3, pos = 2
,通过 keyIndex
查找出 k = 5
由于 k > pos
所以 pos = k = 5
,更新后,记录位置,更新变量,patched++, pos = 5, source[k - j] = source[5] = 3
当 i = 4
,patched = 4, pos = 5
,通过 keyIndex
查找出 k = 4
由于 k < pos
所以 pos = 5
,更新后,记录位置,更新变量,patched++, pos = 5, source[k - j] = source[4] = 4
当 i = 5
,patched = 5, pos = 5
,通过 keyIndex
查找出 k = 3
由于 k < pos
所以 pos = 5
,更新后,记录位置,更新变量,patched++, pos = 5, source[k - j] = source[3] = 5
遍历结束,此时 source = [ 1, 0, 2, 5, 4, 3 ], move = true
通过 seq = lis(source) = [0, 2, 5], k = seq.length - 1 = 3
从 nextLeft - 1
开始遍历
当 i = 5
,source[5] !== -1
但 i === seq[k]
所以不做处理,k--
当 i = 4
,source[4] !== -1
由于 i !== seq[k]
所以要移动该节点,pos = j + i = 4
next[pos]
需要插入在 next[pos + 1]
之前
当 i = 3
,source[4] !== -1
由于 i !== seq[k]
所以要移动该节点,pos = j + i = 3
next[pos]
需要插入在 next[pos + 1]
之前
当 i = 2
,source[5] !== -1
但 i === seq[k]
所以不做处理,k--
当 i = 1
,source[4] !== -1
由于 i !== seq[k]
所以要移动该节点,pos = j + i = 1
next[pos]
需要插入在 next[pos + 1]
之前
当 i = 0
,source[5] !== -1
但 i === seq[k]
所以不做处理,k--
测试代码
1 |
|
执行结果