你需要掌握的树形结构转换方法

88 阅读4分钟

前言

你不一定用过但一定见过的树形结构,如下图:

image.png
这是典型的树形结构。这棵树有很多个节点,其中各个节点之间不是平等的关系,而是有分上下级的,比如这里的Trunk 0-0的子节点有Leaf、Branch 0-0-2,且Branch 0-0-2下也有自己的子节点Leaf。

定义

树形结构是一种由节点组成的层次化数据结构,其中节点通过明确的父子关系形成嵌套,且每个节点(除根节点外)都有且仅有一个父节点

使用场景

如果你接触过后台管理系统,你肯定见过这样的菜单管理:
image.png
又或者是部门管理:

image.png

没错,这些都是树形结构的典型应用场景。


那么在现在组件库如此丰满的情况下,实现这样的树形结构真是现拿现用,打开官网找到跟tree组件相关的效果,直接cv几秒钟不就实现了吗?
是的。对于这种我们无需关心传递的数据结构的场景下,我们只需要在组件库挑选合适的树形效果,复制其示例代码到我们项目中,然后按照它的使用方式定义方法、传相应的数据结构即可。

但是在某些情况下,数据列表的结构不是我们预期的树形结构,而是普通无嵌套结构的list数组,这时候就需要我们将普通的list数组转换成树形结构的数组。

比如项目中使用表格形式的列表展示:

image.png
当然这里的表格展示的是树形数据,在很多情况下,我们表格的数据是请求后端得到的,不是我们预先可以定义好的结构。而后端返回的数据格式往往是普通的数组,我们仍需要额外的处理。

list转Tree

后端返回给我们的表格展示数据通常是这样的:

const data = {
    {id: 1, name: '节点1'},
    {id: 2, name: '节点2'},
    {id: 3, name: '节点3'}
}

我们需要将其转为树形结构然后传给树形组件,如下所示:

const tree = [
    {
        id: 1,
        name: '节点1',
        children: [
            {id: 2, name: '节点2'},
            {id: 3, name: '节点3'}
        ]
    }
]

方法1:Map键值对映射

接下来我们定义一个handleTree函数专门用来做树形转换,定义一个函数时,要明确接收的参数有哪些。

参数:

  • data: 数据源
  • id:id字段,默认值为'id'
  • parentId:父节点字段,默认值为'parentId'
  • children:孩子节点字段,默认值为'children'
function handleTree(data, id, parentId, children){
    //定义配置项,用来准备默认值
    const config = {
        data: data || [],
        id: id || 'id',
        parentId: parentId || 'parentId',
        children: children || 'children'
    }
    //定义孩子节点的映射表
    let childrenListMap = {}
    //定义最终返回的树形结构
    let tree = []
    for(const d of data){
        //获取当前对象的id值
        const id = d[config.id]
        //将当前对象存储到孩子节点映射表中,此时key为当前对象的id,值为当前对象
        childrenListMap[id] = d
        if(!d[config.children]){
            //当前对象无孩子节点,初始化children
            d[config.children] = []
        }
    }
    
    for(const d of data){
        //获取当前对象的父节点id
        const parentId = d[config.parentId]
        //通过parentId到childrenListMap中找到相同id对象的父节点
        const parentObj = childrenListMap[parentId]
        if(!parentObj){
            //如果没找到,说明自身是一级节点,直接存储到tree中
            tree.push(d)
        } else {
            //找到父节点,将其推到父节点的孩子列表中
            parentObj[config.children].push(d)
        }
    }
    
    //返回转换后的结果
    return tree
}

在一般情况下我们只需要传递data数据源给这个函数就行了,那么其他像id、parentId、children这些参数都为config里的默认值。 像上边这种转换方式就是用到了Map键值对的映射方式,通过当前节点的parentIdChildrenListMap中相同的id来映射出父节点。大家可以截一张原始列表结构和转换后的树形结构的代码放在这个函数旁边对照着来理解。

方法2:递归

/**
@params:
    - list:源数据
**/
function listToTree(list){
    const tree = []
    for(const node of list){
        //判断节点是否有pid
        if(!node.pid){
            //没pid,说明为一级节点
            //这里进行浅拷贝,为了保证原节点数据不变
            const p = {...node}
            //当前一级节点的孩子节点为调用getChildren函数的返回值
            p.children = getChildren(p.id, list)
        }
    }
    return tree
}

/**
@params:
    - id:父节点的id
    - list:源数据
**/
function getChildren(id, list){
    const children = []
    
    for(const node of list){
        //判断节点的pid和传进来的id是否相等,相对则找到子节点了
        if(node.pid === id){
            //将该节点推到children中
            children.push(node)
        }
    }
    
    //当前节点仍可能为父节点,递归遍历
    for(const node of children){
        const children = getChildren(node.id, children)
        if(children.length){
            node.children = children
        }
    }
    
    return children
}

方法2本质上也是通过判断一级节点的id和子节点pid相等来组成父子关系的层次结构。
找到一级节点后传递idgetChildren函数中去寻找pid和它相同的子节点,但是当前找到的子节点仍可能是父节点,递归往下继续寻找子节点组成父节关系的层次结构。

本文仅介绍以上两种方法,希望大家都能理解和掌握,有其他方法的小伙伴也欢迎评论区晒出你的代码~