两棵树对比

210 阅读5分钟

最近接到一个需求:本地有一棵树结构数据,点击按钮获取后端最新的树结构数据来跟本地数据对比,需要始终保持两棵树的长度(高度)一致;即有新增项左侧树会空出对应行,有删除项右侧会空出对应行,增删改的差异项都要高亮显示。

先给大家看看效果。 1688968429339.jpg

如图,右侧树橙色高亮项都是表示有差异的,且可勾选,无差异项则无需勾选,我们置灰即可。图例是只有最末级才有差异,但实际情况是任意层级都可能有差异,即可能出现整层级的新增删除的情况。

其实这个需求最核心的问题是如何保证两棵树的高度始终一致?也就是长度一致,要保证树结构的所有的同级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 方法来专门处理各节点的新增删除。

大家不妨拿这段代码去试试看看效果。

最后,感谢观看。