最近接到一个需求:本地有一棵树结构数据,点击按钮获取后端最新的树结构数据来跟本地数据对比,需要始终保持两棵树的长度(高度)一致;即有新增项左侧树会空出对应行,有删除项右侧会空出对应行,增删改的差异项都要高亮显示。
先给大家看看效果。
如图,右侧树橙色高亮项都是表示有差异的,且可勾选,无差异项则无需勾选,我们置灰即可。图例是只有最末级才有差异,但实际情况是任意层级都可能有差异,即可能出现整层级的新增删除的情况。
其实这个需求最核心的问题是如何保证两棵树的高度始终一致?也就是长度一致,要保证树结构的所有的同级children的长度都统一,很明显是需要用到递归了。
识别修改
我们就先从修改开始识别出修改,两侧对应节点的内容不一样我们就视为是修改,需要我们高亮标记出来。
假设我们的左侧本地树数据如下:
let leftTree = [
{
level: 1,
key: '1-1',//唯一值
title: '第一层1-左侧',
},
{
level: 1,
key: '1-2',
title: '第一层2'
},
];
右侧树:
let rightTree = [
{
level: 1,
key: '1-1',
title: '第一层1-右侧',
},
{
level: 1,
key: '1-2',
title: '第一层2'
},
];
这是一种最基础最简单的情况,同层级且下标一致、key一致的,我们就对比两个数组的数据的 title 是否一致,很容易就可以对比出差异。
leftTree.map((left, index) => {
let right = rightTree[index];
if (left.key === right.key && left.title === right.title) {
right.disabled = true;//无差异的复选框禁止勾选
} else {
right.class = 'change-color';//有差异的高亮表示
}
});
当然,这只是最理想的情况,实际可能是两个数组的长度并不相等,即有新增有删除的情况。那么我们就得先想办法把两个数组的长度让他们保持一致。
识别新增
在左侧数组不变的情况下,我们在右侧新增一条数据,使其变成:
let rightTree = [
{
level: 1,
key: '1-1',
title: '第一层1-右侧',
},
{
level: 1,
key: '1-2',
title: '第一层2'
},
{
level: 1,
key: '1-3',
title: '第一层3'
},
];
左侧数组长度为2右侧数组长度为3,右侧比左侧长,很明显这是新增的情况,需要左侧新增数据也要保持一样的长度,我们可以通过获取右侧数据key的集合再遍历左侧数组判断当前元素的key是否包含在右侧key集合里,包含的在右侧key集合里删掉,剩下的就都是不包含的,也就是要新增的。
let rightKeys = rightTree.map(item => item.key);//右侧key集合
leftTree.map((left, index) => {
if (rightKeys.includes(left.key)) {//左右两侧都有该节点
for (let i in rightKeys) {
let key = rightKeys[i];
if (key === left.key) {
rightKeys.splice(i, 1);//删除该key,最终rightKeys里只存在要新增的节点的key
break;
}
}
}
});
//将新增项追加到左侧树最后
rightKeys.map(key => {
leftTree.push({
key,
title: '',//新增的左侧在对比前 title 是没有内容的
});
});
识别删除
新增项我们已经可以识别出来了,那么我们再来识别删除项。删除项是左侧树该数据存在,右侧树该数据不存在,即左侧数据多于右侧。
那么左侧数据为:
let leftTree = [
{
level: 1,
key: '1-1',
title: '第一层1-左侧',
},
{
level: 1,
key: '1-2',
title: '第一层2'
},
{
level: 1,
key: '1-3',
title: '第一层3'
},
];
右侧数据:
let rightTree = [
{
level: 1,
key: '1-1',
title: '第一层1-右侧',
},
{
level: 1,
key: '1-2',
title: '第一层2'
},
];
然后我们把上面循环的那段代码拿过来稍微改造下。
leftTree.map((left, index) => {
if (rightKeys.includes(left.key)) {//左右两侧都有该节点
for (let i in rightKeys) {
let key = rightKeys[i];
if (key === left.key) {
rightKeys.splice(i, 1);//删除该key,最终rightKeys里只存在要新增的节点的key
break;
}
}
} else {//右侧没有该节点,删除情况
let temp = JSON.parse(JSON.stringify(left));
temp.class = 'change-color';//高亮右侧差异项
temp.status = 'delete';//标记为删除项
rightTree.splice(index, 0, temp);
}
});
最后,我们将识别的修改也给整合进来,最后的代码:
leftTree.map((left, index) => {
let right = rightTree[index];
if (left.key === right.key && left.title === right.title) {
right.disabled = true;//无差异的复选框禁止勾选
} else {
right.class = 'change-color';//有差异的高亮表示
}
if (rightKeys.includes(left.key)) {//左右两侧都有该节点
for (let i in rightKeys) {
let key = rightKeys[i];
if (key === left.key) {
rightKeys.splice(i, 1);//删除该key,最终rightKeys里只存在要新增的节点的key
break;
}
}
} else {//右侧没有该节点,删除情况
let temp = JSON.parse(JSON.stringify(left));
temp.class = 'change-color';//高亮右侧差异项
temp.status = 'delete';//标记为删除项
rightTree.splice(index, 0, temp);
}
});
//将新增项追加到左侧树最后
rightKeys.map(key => {
leftTree.push({
key: key,
title: '',
});
});
这样,我们就能识别出增删改且能同步两侧数组长度了。但是需求是对比两棵树结构数据,那肯定是多层的,我们这段代码只能识别单层的,所以我们还需要再加以完善以匹配需求。
封装改造
既然是树那免不了要递归,我们需要把代码封装进方法里了,我们将该方法命名一下取名为 deepTreeDiff。
function deepTreeDiff(leftTree,rightTree){
let rightKeys = rightTree.map(item => item.key);//右侧key集合
//遍历对齐两侧数组高度
leftTree.map((left, index) => {
if (rightKeys.includes(left.key)) {//左右两侧都有该节点
for (let i in rightKeys) {
let key = rightKeys[i];
if (key === left.key) {
rightKeys.splice(i, 1);//删除该key,最终rightKeys里只存在要新增的节点的key
break;
}
}
} else {//右侧没有该节点,删除情况
let temp = JSON.parse(JSON.stringify(left));
temp.class = 'change-color';//高亮右侧差异项
temp.status = 'delete';//标记为删除项
rightTree.splice(index, 0, temp);
temp.children&&loopTree(temp.children, false);//遍历children
}
});
//将新增项追加到左侧树最后
rightKeys.map(key => {
leftTree.push({
key: key,
title: '',
});
});
//差异对比
leftTree.map((left, index) => {
let right = rightTree[index];
if (left.key === right.key && left.title === right.title && !right.status) {//无差异
right.class = '';
right.disabled = true;
} else {//有差异
right.class = 'change-color';
}
if (left.children) {//左侧节点有children
if (!right.children || !right.children.length) {//若右侧没有数组(即删除)
right.children = JSON.parse(JSON.stringify(left.children));
loopTree(right.children, false);//遍历children
}
deepTreeDiff(left.children, right.children);//子节点的遍历
} else if (right.children) {//左侧有数组右侧没数组(即新增)
left.children = JSON.parse(JSON.stringify(right.children));
loopTree(left.children, true);//遍历children
deepTreeDiff(left.children, right.children);//子节点的遍历
}
});
}
/**
* 遍历树给各节点打上标记
* @tree {array} 树数据
* @flag {boolean} true为新增,false为删除
* */
function loopTree(tree, flag) {
tree.map(item => {
flag ? item.title = '' : item.status = 'delete';
item.children && loopTree(item.children, flag);
});
}
以上代码里,deepTreeDiff 的第一个循环体里专门识别新增删除,使两个数组长度保持一致;第二个循环体专门对比差异,使功能区域划分更清晰。
我们还额外新增了一个 loopTree 方法来专门处理各节点的新增删除。
大家不妨拿这段代码去试试看看效果。
最后,感谢观看。