前言
在前端开发中,处理树形结构的数据是一个常见的需求。无论是展示组织结构、文件目录,还是实现搜索树,都需要将扁平数据转换为树形数据,或者将树形数据转换为扁平数据。本文将介绍如何进行这些转换,并结合 antd 和 React 实现一个丝滑的搜索树。
扁平数据转 tree 形数据
首先,我们来看如何将扁平数据转换为树形数据。假设我们有以下扁平数据结构:
const data = [
{ name: "张三", id: 1 },
{ name: "后端技术专家", id: 3, parentId: 2 },
{ name: "首席架构师", id: 2, parentId: 1 },
{ name: "后端架构师", id: 4, parentId: 3 },
{ name: "后端工程师", id: 5, parentId: 4 },
{ name: "后端菜鸟", id: 6, parentId: 5 },
{ name: "后端小白", id: 7, parentId: 6 },
{ name: "李四", id: 8 },
{ name: "支付宝", id: 9, parentId: 8 },
{ name: "淘宝", id: 10, parentId: 8 },
{ name: "天猫", id: 11, parentId: 8 },
{ name: "钉钉", id: 12, parentId: 8 },
{ name: "花呗", id: 13, parentId: 9 },
{ name: "余额宝", id: 14, parentId: 9 },
{ name: "蚂蚁森林", id: 15, parentId: 9 },
{ name: "企业版", id: 16, parentId: 12 },
{ name: "直播", id: 17, parentId: 10 },
{ name: "店铺", id: 18, parentId: 10 },
{ name: "优惠券", id: 19, parentId: 10 },
{ name: "双十一", id: 20, parentId: 11 },
{ name: "双十二", id: 21, parentId: 11 },
{ name: "年货节", id: 22, parentId: 11 },
];
转换函数
我们可以使用递归的方法将扁平数据转换为树形数据:
function arrayToTree(items: any[], parentId: number | null = null): any[] {
return items
// 筛选出当前层级的节点(根据 parentId 匹配)
.filter(item => item.parentId === parentId)
// 遍历筛选出的节点,为每个节点添加子节点
.map(item => ({
...item,
children: arrayToTree(items, item.id)
}));
}
const treeData = arrayToTree(data);
console.log(JSON.stringify(treeData, null, 2));
输出结果
[
{
"name": "张三",
"id": 1,
"children": [
{
"name": "首席架构师",
"id": 2,
"parentId": 1,
"children": [
{
"name": "后端技术专家",
"id": 3,
"parentId": 2,
"children": [
{
"name": "后端架构师",
"id": 4,
"parentId": 3,
"children": [
{
"name": "后端工程师",
"id": 5,
"parentId": 4,
"children": [
{
"name": "后端菜鸟",
"id": 6,
"parentId": 5,
"children": [
{
"name": "后端小白",
"id": 7,
"parentId": 6,
"children": []
}
]
}
]
}
]
}
]
}
]
}
]
},
{
"name": "李四",
"id": 8,
"children": [
{
"name": "支付宝",
"id": 9,
"parentId": 8,
"children": [
{
"name": "花呗",
"id": 13,
"parentId": 9,
"children": []
},
{
"name": "余额宝",
"id": 14,
"parentId": 9,
"children": []
},
{
"name": "蚂蚁森林",
"id": 15,
"parentId": 9,
"children": []
}
]
},
{
"name": "淘宝",
"id": 10,
"parentId": 8,
"children": [
{
"name": "直播",
"id": 17,
"parentId": 10,
"children": []
},
{
"name": "店铺",
"id": 18,
"parentId": 10,
"children": []
},
{
"name": "优惠券",
"id": 19,
"parentId": 10,
"children": []
}
]
},
{
"name": "天猫",
"id": 11,
"parentId": 8,
"children": [
{
"name": "双十一",
"id": 20,
"parentId": 11,
"children": []
},
{
"name": "双十二",
"id": 21,
"parentId": 11,
"children": []
},
{
"name": "年货节",
"id": 22,
"parentId": 11,
"children": []
}
]
},
{
"name": "钉钉",
"id": 12,
"parentId": 8,
"children": [
{
"name": "企业版",
"id": 16,
"parentId": 12,
"children": []
}
]
}
]
}
]
tree 形数据转扁平数据
接下来,我们来看如何将树形数据转换为扁平数据。假设我们有以下树形数据结构:
const treeData = [
{
name: "张三",
id: 1,
children: [
{
name: "首席架构师",
id: 2,
parentId: 1,
children: [
{
name: "后端技术专家",
id: 3,
parentId: 2,
children: [
{
name: "后端架构师",
id: 4,
parentId: 3,
children: [
{
name: "后端工程师",
id: 5,
parentId: 4,
children: [
{
name: "后端菜鸟",
id: 6,
parentId: 5,
children: [
{
name: "后端小白",
id: 7,
parentId: 6,
children: []
}
]
}
]
}
]
}
]
}
]
}
]
},
{
name: "李四",
id: 8,
children: [
{
name: "支付宝",
id: 9,
parentId: 8,
children: [
{
name: "花呗",
id: 13,
parentId: 9,
children: []
},
{
name: "余额宝",
id: 14,
parentId: 9,
children: []
},
{
name: "蚂蚁森林",
id: 15,
parentId: 9,
children: []
}
]
},
{
name: "淘宝",
id: 10,
parentId: 8,
children: [
{
name: "直播",
id: 17,
parentId: 10,
children: []
},
{
name: "店铺",
id: 18,
parentId: 10,
children: []
},
{
name: "优惠券",
id: 19,
parentId: 10,
children: []
}
]
},
{
name: "天猫",
id: 11,
parentId: 8,
children: [
{
name: "双十一",
id: 20,
parentId: 11,
children: []
},
{
name: "双十二",
id: 21,
parentId: 11,
children: []
},
{
name: "年货节",
id: 22,
parentId: 11,
children: []
}
]
},
{
name: "钉钉",
id: 12,
parentId: 8,
children: [
{
name: "企业版",
id: 16,
parentId: 12,
children: []
}
]
}
]
}
];
转换函数
我们可以使用递归的方法将树形数据转换为扁平数据:
function treeToArray(tree: any[]): any[] {
let result: any[] = [];
tree.forEach(node => {
// 解构出当前节点的 children 属性,其余属性保留
const { children, ...rest } = node;
// 将当前节点(去掉 children 属性)添加到结果数组中
result.push(rest);
// 如果存在子节点,递归处理子节点并将结果合并到结果数组中
if (children) {
result = result.concat(treeToArray(children));
}
});
return result;
}
const flatData = treeToArray(treeData);
console.log(JSON.stringify(flatData, null, 2));
输出结果
[
{ "name": "张三", "id": 1 },
{ "name": "首席架构师", "id": 2, "parentId": 1 },
{ "name": "后端技术专家", "id": 3, "parentId": 2 },
{ "name": "后端架构师", "id": 4, "parentId": 3 },
{ "name": "后端工程师", "id": 5, "parentId": 4 },
{ "name": "后端菜鸟", "id": 6, "parentId": 5 },
{ "name": "后端小白", "id": 7, "parentId": 6 },
{ "name": "李四", "id": 8 },
{ "name": "支付宝", "id": 9, "parentId": 8 },
{ "name": "花呗", "id": 13, "parentId": 9 },
{ "name": "余额宝", "id": 14, "parentId": 9 },
{ "name": "蚂蚁森林", "id": 15, "parentId": 9 },
{ "name": "淘宝", "id": 10, "parentId": 8 },
{ "name": "直播", "id": 17, "parentId": 10 },
{ "name": "店铺", "id": 18, "parentId": 10 },
{ "name": "优惠券", "id": 19, "parentId": 10 },
{ "name": "天猫", "id": 11, "parentId": 8 },
{ "name": "双十一", "id": 20, "parentId": 11 },
{ "name": "双十二", "id": 21, "parentId": 11 },
{ "name": "年货节", "id": 22, "parentId": 11 },
{ "name": "钉钉", "id": 12, "parentId": 8 },
{ "name": "企业版", "id": 16, "parentId": 12 }
]
场景
在实际开发中,树形结构的数据广泛应用于各种场景,例如:
- 文件目录:展示文件系统中的目录结构。
- 菜单导航:展示网站或应用中的多级菜单。
- 权限管理:展示用户权限的层级关系。
结合 antd 和 React 实现丝滑的 search Tree(搜索树)
接下来,我们将结合 antd 和 React 实现一个丝滑的搜索树。首先,我们需要安装 antd:
npm install antd
示例代码
以下是一个完整的示例代码,展示如何使用 antd 和 React 实现搜索树:
import React, { useState } from 'react';
import { Tree, Input } from 'antd';
import type { DataNode } from 'antd/es/tree';
const { Search } = Input;
// 原始数据
const data = [
{ name: "张三", id: 1 },
{ name: "后端技术专家", id: 3, parentId: 2 },
{ name: "首席架构师", id: 2, parentId: 1 },
{ name: "后端架构师", id: 4, parentId: 3 },
{ name: "后端工程师", id: 5, parentId: 4 },
{ name: "后端菜鸟", id: 6, parentId: 5 },
{ name: "后端小白", id: 7, parentId: 6 },
{ name: "李四", id: 8 },
{ name: "支付宝", id: 9, parentId: 8 },
{ name: "淘宝", id: 10, parentId: 8 },
{ name: "天猫", id: 11, parentId: 8 },
{ name: "钉钉", id: 12, parentId: 8 },
{ name: "花呗", id: 13, parentId: 9 },
{ name: "余额宝", id: 14, parentId: 9 },
{ name: "蚂蚁森林", id: 15, parentId: 9 },
{ name: "企业版", id: 16, parentId: 12 },
{ name: "直播", id: 17, parentId: 10 },
{ name: "店铺", id: 18, parentId: 10 },
{ name: "优惠券", id: 19, parentId: 10 },
{ name: "双十一", id: 20, parentId: 11 },
{ name: "双十二", id: 21, parentId: 11 },
{ name: "年货节", id: 22, parentId: 11 },
];
// 将扁平数组转换为树形结构
function arrayToTree(items: any[], parentId: number | null = null): DataNode[] {
return items
.filter(item => item.parentId === parentId) // 筛选出当前层级的节点
.map(item => ({
title: item.name, // 节点显示的名称
key: item.id, // 节点的唯一标识
children: arrayToTree(items, item.id), // 递归生成子节点
}));
}
// 转换后的树形数据
const treeData = arrayToTree(data);
const SearchTree: React.FC = () => {
// 状态:存储展开的节点 key
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
// 状态:存储搜索框的值
const [searchValue, setSearchValue] = useState('');
// 状态:是否自动展开父节点
const [autoExpandParent, setAutoExpandParent] = useState(true);
// 处理节点展开事件
const onExpand = (expandedKeys: React.Key[]) => {
setExpandedKeys(expandedKeys); // 更新展开的节点 key
setAutoExpandParent(false); // 手动展开时不自动展开父节点
};
// 处理搜索框内容变化事件
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; // 获取输入框的值
const expandedKeys = treeData
.map(item => {
// 如果节点的标题包含搜索值,则获取其父节点的 key
if (item.title.indexOf(value) > -1) {
return getParentKey(item.key, treeData);
}
return null;
})
// 去重并过滤掉 null 值
.filter((item, i, self) => item && self.indexOf(item) === i);
setExpandedKeys(expandedKeys as React.Key[]); // 更新展开的节点 key
setSearchValue(value); // 更新搜索值
setAutoExpandParent(true); // 搜索时自动展开父节点
};
// 获取父节点的 key
const getParentKey = (key: React.Key, tree: DataNode[]): React.Key | null => {
let parentKey: React.Key | null = null;
for (const node of tree) {
if (node.children) {
// 如果当前节点的子节点中包含目标 key,则当前节点是目标的父节点
if (node.children.some(child => child.key === key)) {
parentKey = node.key;
} else {
// 递归查找子节点
const foundKey = getParentKey(key, node.children);
if (foundKey) {
parentKey = foundKey;
}
}
}
}
return parentKey;
};
// 遍历树形数据,生成带有高亮搜索值的节点
const loop = (data: DataNode[]): DataNode[] =>
data.map(item => {
const index = item.title.indexOf(searchValue); // 搜索值在标题中的位置
const beforeStr = item.title.substring(0, index); // 搜索值之前的部分
const afterStr = item.title.substring(index + searchValue.length); // 搜索值之后的部分
const title =
index > -1 ? (
// 如果标题中包含搜索值,则高亮显示搜索值
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
// 否则正常显示标题
<span>{item.title}</span>
);
if (item.children) {
// 如果有子节点,递归处理子节点
return { ...item, title, children: loop(item.children) };
}
return { ...item, title }; // 返回处理后的节点
});
return (
<div>
{/* 搜索框 */}
<Search style={{ marginBottom: 8 }} placeholder="Search" onChange={onChange} />
{/* 树形控件 */}
<Tree
onExpand={onExpand} // 节点展开事件
expandedKeys={expandedKeys} // 当前展开的节点 key
autoExpandParent={autoExpandParent} // 是否自动展开父节点
treeData={loop(treeData)} // 树形数据,带有高亮搜索值
/>
</div>
);
};
export default SearchTree;
代码解释
-
导入依赖:
React和useState:用于创建 React 组件和管理状态。Tree和Input:从antd库中导入的组件,用于展示树形结构和搜索框。DataNode:antd库中定义的树节点类型。
-
定义扁平数据:
data数组包含了多个对象,每个对象代表一个节点,包含name、id和parentId属性。
-
将扁平数据转换为树形数据:
arrayToTree函数递归地将扁平数据转换为树形数据结构。treeData变量存储转换后的树形数据。
-
定义
SearchTree组件:- 使用
useState定义三个状态变量:expandedKeys、searchValue和autoExpandParent。 onExpand函数处理节点展开事件,更新expandedKeys和autoExpandParent状态。onChange函数处理搜索框内容变化事件,更新expandedKeys和searchValue状态。getParentKey函数递归地获取父节点的key。loop函数遍历树形数据,生成带有高亮搜索值的节点。- 返回包含
Search和Tree组件的 JSX 结构。
- 使用
性能优化
为了提高性能,可以在数据量较大时使用虚拟滚动技术,减少 DOM 节点的渲染数量。此外,可以使用 useMemo 和 useCallback 来缓存计算结果和函数,避免不必要的重新渲染。
常见问题及解决方案
-
数据量大时渲染缓慢:
- 解决方案:使用虚拟滚动技术,如
react-virtualized或react-window。
- 解决方案:使用虚拟滚动技术,如
-
搜索时高亮显示不准确:
- 解决方案:确保在搜索时正确处理大小写和特殊字符。
结语
通过本文的介绍,我们了解了如何将扁平数据转换为树形数据,反之亦然,并结合 antd 和 React 实现了一个丝滑的搜索树组件。希望这些内容对你有所帮助,在实际项目中能够灵活运用这些技巧。