前端开发中会经常用到树形结构数据,如多级菜单、商品的多级分类等。数据库的设计和存储都是扁平结构,就会用到各种Tree树结构的转换操作,本文就尝试全面总结一下。
如下示例数据,关键字段id
为唯一标识,pid
为父级id
,用来标识父级节点,实现任意多级树形结构。"pid": 0
“0”标识为根节点,orderNum
属性用于控制排序。
const data = [
{ "id": 1, "name": "用户中心", "orderNum": 1, "pid": 0 },
{ "id": 2, "name": "订单中心", "orderNum": 2, "pid": 0 },
{ "id": 3, "name": "系统管理", "orderNum": 3, "pid": 0 },
{ "id": 12, "name": "所有订单", "orderNum": 1, "pid": 2 },
{ "id": 14, "name": "待发货", "orderNum": 1.2, "pid": 2 },
{ "id": 15, "name": "订单导出", "orderNum": 2, "pid": 2 },
{ "id": 18, "name": "菜单设置", "orderNum": 1, "pid": 3 },
{ "id": 19, "name": "权限管理", "orderNum": 2, "pid": 3 },
{ "id": 21, "name": "系统权限", "orderNum": 1, "pid": 19 },
{ "id": 22, "name": "角色设置", "orderNum": 2, "pid": 19 },
];
在前端使用的时候,如树形菜单、树形列表、树形表格、下拉树形选择器等,需要把数据转换为树形结构数据,转换后的数据结效果图:
预期的树形数据结构:多了children
数组存放子节点数据。
const treeData = [
{ "id": 1, "name": "用户中心", "pid": 0 },
{
"id": 2, "name": "订单中心", "pid": 0,
"children": [
{ "id": 12, "name": "所有订单", "pid": 2 },
{ "id": 14, "name": "待发货", "pid": 2 },
{ "id": 15, "name": "订单导出","pid": 2 }
]
},
{
"id": 3, "name": "系统管理", "pid": 0,
"children": [
{ "id": 18, "name": "菜单设置", "pid": 3 },
{
"id": 19, "name": "权限管理", "pid": 3,
"children": [
{ "id": 21, "name": "系统权限", "pid": 19 },
{ "id": 22, "name": "角色设置", "pid": 19 }
]
}
]
}
]
列表转树-list2Tree
方法一 递归遍历
从根节点递归,查找每个节点的子节点,直到叶子节点(没有子节点)
//递归函数,pid默认0为根节点
function listToTree(items, pid = 0) {
//查找pid子节点
let pitems = items.filter(s => s.pid === pid)
if (!pitems || pitems.length <= 0)
return null
//递归
pitems.forEach(item => {
const res = buildTree(items, item.id)
if (res && res.length > 0)
item.children = res
})
return pitems
}
方法二 object的Key遍历
简单理解就是一次性循环遍历查找所有节点的父节点,两个循环就搞定了。
- 第一次循环,把所有数据放入一个Object对象map中,id作为属性key,这样就可以快速查找指定节点了。
- 第二个循环获取根节点、设置父节点。
分开两个循环的原因是无法完全保障父节点数据一定在前面,若循环先遇到子节点,map中还没有父节点的,否则一个循环也是可以的。
/**
* 集合数据转换为树形结构。option.parent支持函数,示例:(n) => n.meta.parentName
* @param {Array} list 集合数据
* @param {Object} option 对象键配置,默认值{ key: 'id', parent: 'pid', children: 'children' }
* @returns 树形结构数据tree
*/
export function listToTree(list, option = { key: 'id', parent: 'pid', children: 'children' }) {
let tree = []
// 获取父编码统一为函数
let pvalue = typeof (option.parent) === 'function' ? option.parent : (n) => n[option.parent]
// map存放所有对象
let map = {}
list.forEach(item => {
map[item[option.key]] = item
})
//遍历设置根节点、父级节点
list.forEach(item => {
if (!pvalue(item))
tree.push(item)
else {
map[pvalue(item)][option.children] ??= []
map[pvalue(item)][option.children].push(item)
}
})
return tree
}
树转列表-tree2List
从上而下依次遍历,把所有节点都放入一个数组中即可
/**
* 树形转平铺list(广度优先,先横向再纵向)
* @param {*} tree 一颗大树
* @param {*} option 对象键配置,默认值{ children: 'children' }
* @returns 平铺的列表
*/
export function tree2List(tree, option = { children: 'children' }) {
const list = []
const queue = [...tree]
while (queue.length) {
const item = queue.shift()
if (item[option.children]?.length > 0)
queue.push(...item[option.children])
list.push(item)
}
return list
}
搜索过滤树-filterTree
基本思路:
- 为避免污染原有Tree数据,这里的对象都使用了简单的浅拷贝
const newNode = { ...node }
。 - 递归为主的思路,子节点有命中,则会包含父节点,当然父节点的
children
会被重置。
/**
* 递归搜索树,返回新的树形结构数据,只要子节点命中保留其所有上级节点
* @param {Array|Tree} tree 一颗大树
* @param {Function} func 过滤函数,参数为节点对象
* @param {Object} option 对象键配置,默认值{ children: 'children' }
* @returns 过滤后的新 newTree
*/
export function filterTree(tree, func, option = { children: 'children' }) {
let resTree = []
if (!tree || tree?.length <= 0) return null
tree.forEach(node => {
if (func(node)) {
// 当前节点命中
const newNode = { ...node }
if (node[option.children])
newNode[option.children] = null //清空子节点,后面递归查询赋值
const cnodes = filterTree(node[option.children], func, option)
if (cnodes && cnodes.length > 0)
newNode[option.children] = cnodes
resTree.push(newNode)
}
else {
// 如果子节点有命中,则包含当前节点
const fnode = filterTree(node[option.children], func, option)
if (fnode && fnode.length > 0) {
const newNode = { ...node, [option.children]: null }
newNode[option.children] = fnode
resTree.push(newNode)
}
}
})
return resTree
}