前端开发中会经常用到树形结构数据,如多级菜单、商品的多级分类等。数据库的设计和存储都是扁平结构,就会用到各种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 }
- ]
- }
- ]
- }
- ]
从根节点递归,查找每个节点的子节点,直到叶子节点(没有子节点)
- //递归函数,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 = listToTree(items, item.id)
- if (res && res.length > 0)
- item.children = res
- })
- return pitems
- }
简单理解就是一次性循环遍历查找所有节点的父节点,两个循环就搞定了。
分开两个循环的原因是无法完全保障父节点数据一定在前面,若循环先遇到子节点,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
- }
从上而下依次遍历,把所有节点都放入一个数组中即可
- /**
- * 树形转平铺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
- }
基本思路:
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
- }