效果
以下代码为例:
<script setup>
import {
reactive
} from "vue";
let numberList = reactive([
{id: 1, number: 1000},
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}
]);
function changeNum() {
numberList[1] = {id: 6, number: 6000};
numberList[3] = {id: 7, number: 7000};
numberList[4] = {id: 8, number: 8000};
numberList.push({id: 5, number: 5000});
}
</script>
<template>
<div>
<button @click="changeNum">changeNum</button>
<ul>
<li v-for="c in numberList" :key="c.id">{{ c.number }}</li>
</ul>
</div>
</template>
前后数据对比:
修改前:
{id: 1, number: 1000},
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}
修改后:
{id: 1, number: 1000},
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},
{id: 5, number: 5000},
过程
断点打在patchKeyedChildren
c1:老节点(vnode),c2:新节点;这里3个指针,i=0(从前往后遍历),e1=4、e2=5(从后往前遍历)
1、从前往后
c1[0] 和 c2[0] 判断是同一个节点,执行patch去比较节点内容并判断是否修改,i++;
当 i=1 的时候,c1[1]、c2[1] key值不同直接break;
此时还剩以下节点未比对
旧vnode:
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}
新vnode:
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},
{id: 5, number: 5000},
2、从后往前
c1[4] 和 c2[5]是同一节点进入patch比对vnode内容,e1--、e2--;
e1=3、e2=4的时候不是同一节点直接break;
此时还剩以下节点未比对
旧vnode:
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
新vnode:
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},
3、乱序
经过前两次循环,新老vnode开头第一个和倒数第一个均已比对完毕后续不再考虑,现在3个指针i=1、e1=3,e2=4\
(1)找出旧vnode与新vnode key相同的节点
新vnode还剩4个节点未比对,将节点key与 数组索引 存放在keyToNewIndexMap中\
遍历旧vnode,找keyToNewIndexMap(新vnode key的map结构)中是否存在相同key;\
新vnode中没有id==2或4的,从dom结构中删除这个节点
有id==3的,用newIndexToOldIndexMap记录 旧vnode数组索引 和 新vnode数组索引 的映射关系,再执行patch\
此时还剩以下节点未比对
旧vnode:多余的已删除(注:vnode依然存在,只是从父dom结构中删除子dom)
新vnode:
{id: 6, number: 6000},
{id: 7, number: 7000},
{id: 8, number: 8000},
(2)添加旧vnode没有的节点
从后往前添加新vnode中没有对比过的节点,这里从id=8开始,anchor是锚点为了确认节点插在哪,此刻是id=5的节点;
newIndexToOldIndexMap[i] === 0判断该节点是否已经被旧节点相同key对比过了(这里id=3已经对比过了不是0)
至此diff算法已完成
最长递增子序列
写完博客才发现上面的例子过于简单没有触发这个算法
在乱序对比中,遍历旧vnode找到key与新vnode相同的会记录在newIndexToOldIndexMap中
以下为例,这个例子中所有节点都会复用。patch是将新旧节点内容对比,并没有改变真实dom的顺序如果不排序展示在页面上的还是id:1,2,3,4,5这个顺序,所以拿到最长递增子序列用来减少dom结构顺序调整消耗的性能
修改前:
{id: 1, number: 1000},
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}
修改后:
{id: 4, number: 4000},
{id: 1, number: 1000},
{id: 5, number: 5000},
{id: 2, number: 2000},
{id: 3, number: 3000}
newIndexToOldIndexMap数组是 [4,1,5,2,3]
通过getSequence求出数组最长递增子序列的索引,结果[1,3,4] (对应的是key值1,2,3,其他节点都根据这3个节点调整)
下面是源码用到的算法:
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = u + v >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
按 [4,1,5,2,3] 从后往前遍历,遇到id=5或4的时候执行move