最近在写项目的时候,遇到了现有的树状组件不能满足目前的需求,需要对树形组件进行(TreeSelect
)扩展。扩展的功能主要是能够通过输入搜索关键字对树形结构进行过滤,在扩展的过程中遇到了数据结构的相互转换以及树形结构的过滤,这里分享给大家。
1、数据结构
一个典型的树形结构应该是如下的:
const treeData = [
{
id:"p1",
title: '广东',
children: [{
id:"p1-1",
title: '广州',
}]
},
{
id:"p2",
title:"四川",
children:[{
id:"p2-1",
title:"成都",
children: [{
id:"p2-1-1",
title:"高新区",
}]
},
{
id:"p2-2",
title:"德阳"
},
{
id:"p2-3",
title:"绵阳"
}]
}
]
但是有时候后端返回的数据不是上面那样的,而是返回了一个平铺的结构:
const listData = [
{
id:"p1",
title: '广东',
},
{
id:"p1-1",
pid: 'p1',
title: '广州',
},
{
id:"p2",
title:"四川",
},
{
id:"p2-1",
pid: 'p2',
title:"成都",
},
{
id:"p2-2",
pid: 'p2',
title:"德阳"
},
{
id:"p2-3",
pid: 'p2',
title:"绵阳"
},
{
id:"p2-1-1",
pid: 'p2-1',
title:"高新区",
}
]
这时候需要我们自己根据id
和pid
遍历数据生成一个树形的结构。当然有些时候我们也需要将树形结构转换为扁平的数组形式。
2、list转tree
2.1、递归
使用递归的方式来完成list到tree的转换
function list2tree(list) {
const tree = []
for(const node of list) {
// 如果没有pid就可以认为是根节点
if(!node.pid) {
let p = { ...node }
p.children = getChildren(p.id, list)
tree.push(p)
}
}
function getChildren(id, list) {
const children = []
for(const node of list) {
if(node.pid === id) {
children.push(node)
}
}
for(const node of children) {
const children = getChildren(node.id, list)
if(children.length) {
node.children = children
}
}
return children
}
return tree
}
2.2、双层循环
使用两次循环,首先遍历每个节点将其作为父节点(parentNode
),然后再次遍历数据判断数据的pid
是否等于parentNode
的id
。
function list2tree(list) {
list.forEach(child => {
const pid = child.pid
if(pid) {
list.forEach(parent => {
if(parent.id === pid) {
parent.children = parent.children || []
parent.children.push(child)
}
})
}
})
return list.filter(n => !n.pid)
}
2.3 使用 map
export function list2tree(list) {
const [map, treeData] = [{}, []];
for (let i = 0; i < list.length; i += 1) {
map[list[i].id] = i;
list[i].children = [];
}
for (let i = 0; i < list.length; i += 1) {
const node = list[i];
if (node.pid && list[map[node.pid]]) {
list[map[node.pid]].children.push(node);
} else {
treeData.push(node);
}
}
return treeData;
}
3、tree转list
树形结构转换为数组就涉及到树的遍历了,树的遍历分为深度遍历(前中后序)和广度遍历,下面分别使用深度遍历和广度遍历来遍历树并转为数组。
3.1、广度遍历
沿着树的宽度遍历节点。采用队列来辅助完成广度遍历。
function tree2list(tree) {
const list = []
const queue = [...tree]
while(queue.length) {
const node = queue.shift()
const children = node.children
if(children) {
queue.push(...children)
}
list.push(node)
}
return list
}
3.2、深度遍历
沿着树的深度遍历。采用栈来辅助完成深度遍历。
function tree2list(tree) {
const list = []
const stack = [...tree]
while(stack.length) {
const node = stack.pop()
const children = node.children
if(children) {
queue.push(...children)
}
list.push(node)
}
return list
}
4、树的过滤
通过传入关键字来对树进行过滤,这里主要涉及到两种情况:
- 将满足条件的数据放在一个空数组里,然后将数组返回,这样就破坏了原来的树形结构
- 过滤后的数据保持原来的树形结构,如果一个节点满足筛选条件那么需要将它的父节点也返回
对于第一种情况直接返回数组的只要遍历树然后返回满足条件的即可。
第二种情况需要保留原来树形结构的: 方法一:
function filterTree(tree, key) {
const filterChildrenTree = (resTree, treeItem) => {
if (treeItem.title.includes(key)) {
resTree.push(treeItem)
return resTree
}
if (Array.isArray(treeItem.children)) {
const children = treeItem.children.reduce(filterChildrenTree, []);
const data = { ...treeItem, children }
if (children.length) {
resTree.push({ ...data })
}
}
return resTree;
};
return tree.reduce(filterChildrenTree, []);
}
方法二:
/**
* 过滤树,并保留原有的结构
* @param treeData
* @param predicate
* @returns
*/
export function filterTree<T extends TreeNode>(treeData: T[], predicate: (data: T) => boolean) {
function filter(treeData: T[] | undefined): T[] | undefined {
if (!treeData?.length) {
return treeData;
}
return treeData.filter((data) => {
if (!predicate(data)) {
return false;
}
data.children = filter(data.children);
return true;
});
}
return filter(treeData) || [];
}