前端树形Tree数据结构使用-🤸🏻♂️各种姿势总结

01、树形结构数据
前端开发中会经常用到树形结构数据,如多级菜单、商品的多级分类等。数据库的设计和存储都是扁平结构,就会用到各种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数组存放子节点数据。
[
{ "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 }
]
}
]
}
]
02、列表转树-list2Tree
常用的算法有2种:
- 递归遍历子节点:先找出根节点,然后从根节点开始递归遍历寻找下级节点,构造出一颗树,这是比较常用也比较简单的方法,缺点是数据太多递归耗时多,效率不高。还有一个隐患就是如果数据量太,递归嵌套太多会造成JS调用栈溢出,参考《JavaScript函数(2)原理{深入}执行上下文》。
- 2次循环Object的Key值:利用数据对象的
id作为对象的key创建一个map对象,放置所有数据。通过对象的key快速获取数据,实现快速查找,再来一次循环遍历获取根节点、设置父节点,就搞定了,效率更高。
递归遍历
从根节点递归,查找每个节点的子节点,直到叶子节点(没有子节点)。
//递归函数,pid默认0为根节点
function buildTree(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 list2Tree(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
}
- 参数
option为数据结构的配置,就可以兼容各种命名的数据结构了。 option中的parent支持函数,兼容一些复杂的数据结构,如parent: (n) => n.meta.parentName,父节点属性存在一个复合对象内部。
测试一下:
data.sort((a, b) => a.orderNum - b.orderNum)
const sdata = list2Tree(data)
console.log(sdata)
对比一下
| 递归遍历 | object的Key遍历 | |
|---|---|---|
| 时间复杂度 | O(n)最差的情况是n-1个节点都有子节点,就会递归n-1次 | O(2)循环两次 |
| 空间复杂度 | 没有创建额外的非必要对象 | O(n)额外创建了一个map对象,包含了所有节点 |
| 总结 | 容易理解,比较常用,但性能一般 | 借助对象的属性key,比较巧妙,性能高 |
延伸一下:Map和Object哪个更快?
在上面的方案2(object的Key遍历)中使用的是Object,其实也是可以用ES6新增的Map对象。Object、Map都可用作键值查找,速度都还是比较快的,他们内部使用了哈希表(hash table)、红黑树等算法,不过不同引擎可能实现不同。
let obj = {};
obj['key1'] = 'objk1'
console.log(obj.key1)
let map = new Map()
map.set('key1','map1')
console.log(map.get('key1'))
大多数情况下Map的键值操作是要比Object更高效的,比如频繁的插入、删除操作,大量的数据集。相对而言,数据量不多,插入、删除比较少的场景也是可以用Object的。
03、树转列表-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
}
04、设置节点不可用-setTreeDisable
递归设置树形结构中数据的 disabled 属性值为不可用。使用场景:在修改节点所属父级时,不可选择自己及后代。

基本思路:
- 先重置
disabled属性,递归树所有节点,这一步可根据实际情况优化下。 - 设置目标节点及其子节点的
disabled属性。
/**
* 递归设置树形结构中数据的 disabled 属性值为不可用。使用场景:在修改父级时,不可选择自己及后代
* @param {*} tree 一颗大树
* @param {*} disabledNode 需要禁用的节点,就是当前节点
* @param {*} option 对象键配置,默认值{ children: 'children', disabled: 'disabled' }
* @returns void
*/
export function setTreeDisable(tree, disabledNode, option = { children: 'children', disabled: 'disabled' }) {
if (!tree || tree.length <= 0)
return tree
// 递归更新disabled值
const update = function(tree, value) {
if (!tree || tree.length <= 0)
return
tree.forEach(item => {
item[option.disabled] = value
update(item[option.children], value)
})
}
// 开始干活,先重置
update(tree, false)
if (!disabledNode) return tree
// 设置所有子节点disable = true
disabledNode[option.disabled] = true
update(disabledNode[option.children], true)
return tree
}
05、搜索过滤树-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
}
参考资料
- 开源项目库:kvue-admin
- 文中tree源码:tree.js
- elementUI中树形下拉框的实现
️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀
前端树形Tree数据结构使用-🤸🏻♂️各种姿势总结的更多相关文章
- 生成树形结构的json字符串代码(c#)供前端angular tree使用.
框架是使用EF6.0.可以针对返回的值使用Newtonsoft.Json.dll(百度搜一下)来对返回的值序列化为json字符串,如果对以下值那就是使用JsonConvert.SerializeObj ...
- c语言实现tree数据结构
该代码实现了tree的结构.依赖dyArray数据结构.有first一级文件夹.second二级文件夹. dyArray的c实现參考这里点击打开链接 hashTable的c实现參考这里点击打开链接 ...
- 8 个最好的 jQuery 树形 Tree 插件
由于其拥有庞大,实用的插件库,使得 jQuery 变得越来越流行.今天将介绍一些最好的 jQuery 树形视图插件,具有扩展和可折叠的树视图.这些都是轻量级的,灵活的 jQuery 插件,它将一个无序 ...
- 实用的两款jquery树形tree插件
这里有两款非常实用的jquery tree控件: (1) ------------------------------------------1.(根据一讲师总结) ---zTree: jquery. ...
- pat 甲级 1064 ( Complete Binary Search Tree ) (数据结构)
1064 Complete Binary Search Tree (30 分) A Binary Search Tree (BST) is recursively defined as a binar ...
- 8.30前端jQuery和数据结构知识
2018-8-30 16:37:17 单链表的demo 从俺弟家回来了! 发现,还是要努力学习是很重要的!!努力学习新的感兴趣的东西!! 多读书还是很重要的!!! 越努力,越幸运! # coding: ...
- [NOI2014]购票 --- 斜率优化 + 树形DP + 数据结构
[NOI2014]购票 题目描述 今年夏天,NOI在SZ市迎来了她30周岁的生日. 来自全国 n 个城市的OIer们都会从各地出发,到SZ市参加这次盛会. 全国的城市构成了一棵以SZ市为根的有根树,每 ...
- web前端面试系列 - 数据结构(两个栈模拟一个队列)
一. 用两个栈模拟一个队列 思路一: 1. 一个栈s1作为数据存储,另一个栈s2,作为临时数据存储. 2. 入队时将数据压人s1 3. 出队时将s1弹出,并压人s2,然后弹出s2中的顶部数据,最后再将 ...
- C++-POJ3321-Apple Tree[数据结构][树状数组]
树上的单点修改+子树查询 用dfn[u]和num[u]可以把任意子树表示成一段连续区间,此时结合树状数组就好了 #include <set> #include <map> #i ...
- 优化vue+springboot项目页面响应时间:waiting(TTFB) 及content Download
优化vue+springboot项目页面响应时间:waiting(TTFB) 及content Download TTFB全称Time To First Byte,是指网络请求被发起到从服务器接收到地 ...
随机推荐
- [转帖]auto_explain
https://help.kingbase.com.cn/v8/development/sql-plsql/ref-extended-plug-in/auto_explain.html 6.1. 插件 ...
- Kafka学习之四_Grafana监控相关的学习
Kafka学习之四_Grafana监控相关的学习 背景 想一并学习一下kafaka的监控. 又重新开始学习grafana了: 下载地址: https://grafana.com/grafana/dow ...
- [转帖]Guanaco, Llama, Vicuña, Alpaca该怎么区别
https://zhuanlan.zhihu.com/p/106262896 在智利和秘鲁高原区经常会遇到的一种动物让人十分挠头,学术点称呼就是骆驼科其中一个族群--羊驼属和骆马属.头疼在于,分不清楚 ...
- Linux无头模式使用mat分析dump的方法
摘要 mat可以很好的进行jvm的内存dump的分析. 但是大部分服务器是没有GUI界面的. 而且就算是有GUI界面也很难直接使用. 但是随着jvm堆区越来越大. WindowsPC机器已经很难进行分 ...
- Spring缓存是如何实现的?如何扩展使其支持过期删除功能?
前言:在我们的应用中,有一些数据是通过rpc获取的远端数据,该数据不会经常变化,允许客户端在本地缓存一定时间. 该场景逻辑简单,缓存数据较小,不需要持久化,所以不希望引入其他第三方缓存工具加重应用负担 ...
- echarts饼状图自定义legend的样式付费
先看效果图 代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> < ...
- vue和uni-app不同的类型绑定不同的类名
vue不同的类型绑定不同的类名 第一种 <div v-for="(item, index) in list" :key="index" > < ...
- 【发现一个问题】使用 fastcgo 导致额外的 `runtime._System` 调用的消耗
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 为了避免 cgo 调用浪费太多资源,因此使用了 fastc ...
- 【3】超级详细matplotlib使用教程,手把手教你画图!(多个图、刻度、标签、图例等)
相关文章: 全网最详细超长python学习笔记.14章节知识点很全面十分详细,快速入门,只用看这一篇你就学会了! [1]windows系统如何安装后缀是whl的python库 [2]超级详细Pytho ...
- 2.5 Windows驱动开发:DRIVER_OBJECT对象结构
在Windows内核中,每个设备驱动程序都需要一个DRIVER_OBJECT对象,该对象由系统创建并传递给驱动程序的DriverEntry函数.驱动程序使用此对象来注册与设备对象和其他系统对象的交互, ...