终于来到了我最想写的diff了。当时看这一块代码看了好久。然后顺带get了最长上升子序列算法和vue3源码中做的优化。
新子节点数组相对于旧子节点数组的变化, 无非是通过更新, 删除, 添加和移动节点来完成的.
新旧子节点数组很容易拥有相同的头尾节点. 对于相同的节点, 我们只需要对比更新即可.
1.同步头部节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
// 相同的节点,递归执行 patch 更新节点
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
break
}
i++
}
}
2.同步尾部节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
break
}
e1--
e2--
}
}
完成这两次的同步循环以后, 接下来就只有3中情况的处理了:
- 新子节点有剩余要添加的新节点
- 旧子节点有剩余要删除的多余节点
- 未知子序列
3.添加新的节点
首先判断新子节点是否有剩余:
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
// 3. 挂载剩余的新节点
// i = 2, e1 = 1, e2 = 2
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
while (i <= e2) {
// 挂载新节点
patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG)
i++
}
}
}
}
如果索引i
大于尾部索引e1
且i
小于e2
,那么从索引i
开始到索引e2
之间,我们直接挂载新子树这部分的节点。
4.删除多余节点
如果不满足添加新节点的情况, 就要几折判断旧子节点是否有剩余, 如果满足则删除旧子节点, 实现代码如下:
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 4, e2 = 3
// (a b) c d e
// (a b) d e
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 4, e2 = 3
// (a b) c (d e)
// (a b) (d e)
// 3. 普通序列挂载剩余的新节点
// i = 2, e1 = 2, e2 = 1
// 不满足
if (i > e1) {
}
// 4. 普通序列删除多余的旧节点
// i = 2, e1 = 2, e2 = 1
else if (i > e2) {
while (i <= e1) {
// 删除节点
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
}
5.处理未知子序列
对于即不满足新增也不满足删除的情况, 我们需要处理未知子序列
对于子节点的操作, 归结起来就是更新, 删除, 添加和移动.
- 对于两个相同类型的节点, 我们执行更新操作,
- 新节点没有而旧节点有, 执行删除操作.
- 新节点有而旧节点没有, 则执行新增操作.
- 相对比较麻烦的实际上是移动子节点
移动子节点
比如对于这样两个序列:
var prev = [1, 2, 3, 4, 5, 6]
var next = [1, 3, 2, 6, 4, 5]
思路是在next中找到最长的递增子序列; 序列越短,移动元素次数越少,代价越小。
1.建立索引:
keyToNewIndexMap 存储的就是新子序列中每个节点在新子序列中的索引,
旧节点: a b c d e f g h
新节点: a b e c d i g h
得到了一个值为 {e:2,c:3,d:4,i:5} 的新子序列索引图。
2.更新和移除旧节点
我们就需要遍历旧子序列,有相同的节点就通过 patch 更新,并且移除那些不在新子序列中的节点,同时找出是否有需要移动的节点,我们来看一下这部分逻辑的实现:
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 7, e2 = 7
// (a b) c d e f g h
// (a b) e c d i g h
// 2. 从尾部开始同步
// i = 2, e1 = 7, e2 = 7
// (a b) c d e f (g h)
// (a b) e c d i (g h)
// 3. 普通序列挂载剩余的新节点,不满足
// 4. 普通序列删除多余的旧节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子序列开始索引,从 i 开始记录
const s1 = i
// 新子序列开始索引,从 i 开始记录
const s2 = i
// 5.1 根据 key 建立新子序列的索引图
// 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
// 新子序列已更新节点的数量
let patched = 0
// 新子序列待更新节点的数量,等于新子序列的长度
const toBePatched = e2 - s2 + 1
// 是否存在要移动的节点
let moved = false
// 用于跟踪判断是否有节点移动
let maxNewIndexSoFar = 0
// 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长递增子序列
const newIndexToOldIndexMap = new Array(toBePatched)
// 初始化数组,每个元素的值都是 0
// 0 是一个特殊的值,如果遍历完了仍有元素的值为 0,则说明这个新节点没有对应的旧节点
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0
// 正序遍历旧子序列
for (i = s1; i <= e1; i++) {
// 拿到每一个旧子序列节点
const prevChild = c1[i]
if (patched >= toBePatched) {
// 所有新的子序列节点都已经更新,剩余的节点删除
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
// 查找旧子序列中的节点在新子序列中的索引
let newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 找不到说明旧子序列已经不存在于新子序列中,则删除该节点
unmount(prevChild, parentComponent, parentSuspense, true)
}
else {
// 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况,影响对后续最长递增子序列的求解
newIndexToOldIndexMap[newIndex - s2] = i + 1
// maxNewIndexSoFar 始终存储的是上次求值的 newIndex,如果不是一直递增,则说明有移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
}
else {
moved = true
}
// 更新新旧子序列中匹配的节点
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
patched++
}
}
}
然后去倒序遍历newIndexToOldIndexMap; 如果值为0,则代表新增,如果在最长子序列里面就继续遍历;不在则新增;
为什么需要倒序遍历呢? 因为倒序遍历可以方便我们使用最后更新的节点作为锚点。????没太明白这个点。。。
最长递增子序列:
动态规划:
var arr = [2,3,5,10,7,20,100,0];
let dp =[];
for(let i=0;i<arr.length;i++){
dp[i]=1;
}
let res = 0;
for(let i=1;i<arr.length;i++){
for(let j=0;j<i;j++){
if(arr[j]<arr[i]){
dp[i] = Math.max(dp[i],dp[j]+1) // dp[i]代表第i个位置上升子序列中元素的长度
}
}
res = Math.max(res,dp[i])
}
console.log(res)
最小二分法
let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9]
let subArr = [arr[0]]
for (let i = 1; i < arr.length; i++) {
if (arr[i] > subArr[subArr.length - 1]) {
subArr.push(arr[i])
} else {
// arr[i] 较小
let u = 0
let c = 0
let v = subArr.length - 1
// 二分搜索,查找比 arrI 小的节点,更新 result 的值
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[i] > subArr[c]) {
u = c + 1
} else {
v = c
}
}
if (arr[i] < subArr[u]) {
subArr[u] = arr[i]
}
}
}
对于let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9];
subArr 的历程是:1
1,5
1,5,6
1,5,6,7
1,5,6,7,8
1,2,6,7,8
1,2,3,7,8
1,2,3,4,8
1,2,3,4,8,9
虽然序列不正确,但是不影响它的长度的正确性;
这个最小二分法只能求长度;不能得到正确的子序列;
想要拿到正确的最长子序列(可能不是唯一的),就得再维护一个数组;
下面这段代码和上面完全一样,除了subArr存储的意义;上面保存的arr的值;下面的是arr的index 也是for循环里面的i(其实i更好理解一些)
let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9]
let subArr = [0]
let prevIndex=[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > arr[subArr[subArr.length - 1]]) {
prevIndex.push(subArr[subArr.length - 1])
subArr.push(i)
} else {// arr[i] 较小
let u = 0;
let c= 0;
let v = subArr.length - 1
// 二分搜索,查找比 arrI 小的节点,更新 result 的值
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[i] > arr[subArr[c]]) {
u = c + 1
} else {
v = c
}
}
if (arr[i] < arr[subArr[u]]) {
prevIndex.push(subArr[u-1])
subArr[u] = i
}
}
// 利用前驱节点重新计算subArr
let length = subArr.length; //总长度
let prev = subArr[length - 1] // 最后一项
while (length-- > 0) {// 根据前驱节点一个个向前查找
subArr[length] = prev
prev = prevIndex[subArr[length]]
}
}
增加一个数组保存subArr更新前的index;记录result的更新过程;prevIndex数组的第一个没有意义;因为for循环是从1开始的;
prevIndex[i]记录的是subArr在arr[i]更新前记录的上一个值;
subArr最后一个值一定是准确的;当它记录进去的时候它的上一个值也一定是准确的(可能有不同的序列,但一定是某个正确的序列里面的值);
所以通过prevIndex[i]拿到它的上一个准确值;这样递归循环;