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

03-03 1142阅读 0评论

01、树形结构数据

前端开发中会经常用到树形结构数据,如多级菜单、商品的多级分类等。数据库的设计和存储都是扁平结构,就会用到各种Tree树结构的转换操作,本文就尝试全面总结一下。

前端树形Tree数据结构使用-‍♂️各种姿势总结,前端树形Tree数据结构使用-‍♂️各种姿势总结,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,方法,设置,第1张
(图片来源网络,侵删)

如下示例数据,关键字段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 },
];

在前端使用的时候,如树形菜单、树形列表、树形表格、下拉树形选择器等,需要把数据转换为树形结构数据,转换后的数据结效果图:

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

预期的树形数据结构:多了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)原理{深入}执行上下文》。

    前端树形Tree数据结构使用-‍♂️各种姿势总结,前端树形Tree数据结构使用-‍♂️各种姿势总结,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,方法,设置,第3张
    (图片来源网络,侵删)
    • 🟢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  {
          const res = buildTree(items, item.id)
          if (res && res.length > 0)
            item.children = res
        })
        return pitems
      }

      🟢object的Key遍历

      简单理解就是一次性循环遍历查找所有节点的父节点,两个循环就搞定了。

      • 第一次循环,把所有数据放入一个Object对象map中,id作为属性key,这样就可以快速查找指定节点了。

      • 第二个循环获取根节点、设置父节点。

        分开两个循环的原因是无法完全保障父节点数据一定在前面,若循环先遇到子节点,map中还没有父节点的,否则一个循环也是可以的。

        前端树形Tree数据结构使用-‍♂️各种姿势总结,前端树形Tree数据结构使用-‍♂️各种姿势总结,词库加载错误:未能找到文件“C:\Users\Administrator\Desktop\火车头9.8破解版\Configuration\Dict_Stopwords.txt”。,使用,方法,设置,第4张
        (图片来源网络,侵删)
        /**
         * 集合数据转换为树形结构。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)

          对比一下

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

          延伸一下: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 属性值为不可用。使用场景:在修改节点所属父级时,不可选择自己及后代。

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

          基本思路:

          • 先重置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)
                    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
            }

            文章转载自:安木夕

            原文链接:https://www.cnblogs.com/anding/p/17625911.html

            体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构


免责声明
本网站所收集的部分公开资料来源于AI生成和互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,1142人围观)

还没有评论,来说两句吧...

目录[+]