记Vue项目中对于树形数据的处理和展示

3,227 阅读10分钟

在项目中遇到了树形数据的处理展示和筛选,觉得这个思路和处理方法很有必要记录下来一下,其中还出现了深拷贝相关的一些知识,记下来加深印象,防止忘记哦。

1.树形数据的介绍

首先,什么是树形数据呢,这其实是我自己不知道在哪看来的名字,不具有权威性,其实就是当一个数据需要有层级的表示出来的时候,就需要利用树形数据的形式。比如说如图:

image.png

这是一个形容课程分类的数据,最上面一层的数据显然是name从“社团和协会”到“校院学生会组织”,可以看到他们的id依次从1-4,然后他们的pid为0,意思他们没有父亲节点了。
然后我们再往下看一层,也就是id从5-8,然后他们的pid可以看到分别是1-4,什么意思呢?也就是说他们的父亲节点分别是id为1-4的数据。也就是说id=1的“社团和协会”下面有一个子节点,是id=5的"学生工作室"(因为"学生工作室"的pid=1),然后id=5的"学生工作室"肯定也会有其他的子节点,这些子节点的pid=5。
到这里应该就可以理解树形数据的概念了吧,就是每个存在数据库的数据不仅仅有自己的id,也有他父节点的id(pid),这样就可以方便清晰的表示父子之间的层级关系了!

2.树形数据的展示

那么拿到这些数据之后,我们应该怎么把他以层级的形式展示出来呢?
我们可以使用Element组件里面的table表格组件,如图:

image.png

我们点开看看他是怎么用的,发现他需要数组的形式是这样的,如图:

image.png

显然和我们收到的数据格式不符合,他需要把每个数据的孩子节点装在children数组里面,而不是之间用pid来表示父子之间的关系,所以我们需要动手来讲原来的扁平数据转换成真正的树形数据(有多层)。
所以需要我们写个算法来转换一下,显然是递归。代码如下:
export function filterGroupClassificationList(data)
{
    //father_id为各层级的父亲的id
    let filter = (father_id) => {
        let array = []
        //拿到父亲id之后就去data里面遍历
        //看谁的parentId为这个父亲id
        data.forEach(item => {
            if (item.parentId === father_id) {
                //将这个父亲id的所有儿子存到一个数组里面
                array.push(item)
            }
        })
        //再遍历所有儿子
        array.forEach(item => {
            //通过递归传入儿子的id,再去找儿子的儿子
            let temp = filter(item.id)
            //如果儿子的儿子不为空
            //就将儿子的儿子放进儿子的children对象里面
            if (temp.length != 0) {
                item.children = temp
            }
        })
        //然后返回所得数组
        return array
    }
    
    //传入最开始第一层的parentId
    let filterArray = filter(0)
    //最后返回过滤完的数据
    return filterArray
}
我相信我这个注释已经写的很清楚了,算法是好兄弟想出来的,我精简了一下,好兄弟真的厉害!
但是不仅仅是这个,因为数据中有个sort字段要用来排序,而我们还只是把这个数据变成了树形的,但是还没排序,所有我们还得写一个排序函数来给这个树形排序,同样也是递归了,代码如下:
let sortWay = array => {
        //先排序当前数组(按sort来排序)
        array.sort(function(a, b) {
            (a.sort-b.sort意思是按sort从小到大排序)
            return a.sort - b.sort
        })
        //然后遍历当前数组 他们的儿子存在的话就把他们的儿子排序,递归下去
        array.forEach(item => {
            item.children && sortWay(item.children)
        })
        //返回排序之后的数组
        return array
    }
所以把他们合起来之后代码如下:
export function filterGroupClassificationList(data)
{
    //father_id为各层级的父亲的id
    let filter = (father_id) => {
        let array = []
        //拿到父亲id之后就去总数居data里面遍历
        //看谁的parentId为这个父亲id
        data.forEach(item => {
            if (item.parentId === father_id) {
                //将这个父亲id的所有儿子存到一个数组里面
                array.push(item)
            }
        })
        //再遍历所有儿子
        array.forEach(item => {
            //通过递归传入儿子的id,再去找儿子的儿子
            let temp = filter(item.id)
            //如果儿子的儿子不为空
            //就将儿子的儿子放进儿子的children对象里面
            if (temp.length != 0) {
                item.children = temp
            }
        })
        //然后返回所得数组
        return array
    }
    let sortWay = array => {
        //先排序当前数组(按sort来排序)
        array.sort(function(a, b) {
            (a.sort-b.sort意思是按sort从小到大排序)
            return a.sort - b.sort
        })
        //然后遍历当前数组 他们的儿子存在的话就把他们的儿子排序,递归下去
        array.forEach(item => {
            item.children && sortWay(item.children)
        })
        //返回排序之后的数组
        return array
    }
    
    //传入最开始第一层的parentId
    let filterArray = filter(0)
    //
    //最后返回过滤完 且排序完 的数据
    return sortWay(filterArray);
}

排序完成之后我们就可以在项目中引入这个函数然后用来过滤,之后得到的数据如下了:

image.png

可以看出来现在的数据第一层为0,1,2,3,然后0里面还有个children对象,里面装的就是pid=3的数据(因为0数据里面的id=3),所以到现在我们就过滤成功了!
然后让我们来展示出来吧!
element里面的table参数如下:
<el-table
    :data="treeData"
    row-key="id"
    v-loading="loading"
    default-expand-all
    :tree-props="{
        children: 'children',
        hasChildren: 'hasChildren'
    }"
>

效果如图:

image.png

3.其他需求的扩展

你以为到这里就结束了吗?nonono,项目中还有一个需求,就是要能修改同层的所有数据的排序,这是什么意思呢,其实核心问题就是要你找到该数据的所有兄弟们(同一个pid的数组),然后更改他们的排序就ok,那怎么找一个数据的兄弟们呢?有几种方案:

1.第一种是我一瞬间想到了,直接遍历我们这个树,然后找到id等于该数据pid的数据(也就是该元素的父亲),然后父亲的children对象里面装的就是所有兄弟们的信息了。但是这样肯定不好,又要递归遍历整个数,性能可能不太行。pass!

2.然后我仔细一思考,为啥要遍历树呢,为什么我们不能遍历扁平数据呢?也就是一开始没有过滤的数据,他们都在一个对象数组里面装着呢,我们只要遍历这个数组,找到所有pid相等的数据,把他们push到一个数组里面即可了。这样就比遍历数要省事情一些了。

3.然后好兄弟又想到个更省时间复杂度的办法,我堪称奇妙,就是借鉴了vue里面父子组件存储信息的方式,子组件里面还包含一个_parent_对象来存储父亲的数据,这样的话我们找兄弟的数据不只要找该数组的_parent_对象的children对象就可以了吗?

那么有好兄弟想问了:那么顶层的数据呢?他们又没有父亲?

顶层的数据不就是过滤之后的那个数组?他们没有父亲就给他们的_parent_对象赋个null就好了。

所以问题的关键又在于怎么给每个数组都加一个_parent_对象来存储他父节点的数据呢?其实不难,直接在原来递归的算法中加个一个步骤就ok了,代码如下(其实就是传参的时候多传一个父亲对象,然后递归的时候给当前数据赋值他的父亲对象即可):

export function filterGroupClassificationList(data)
{
    //fahter父亲对象,father_id为各层级父亲对象的id
    let filter = (father,father_id) => {
        let array = []
        //拿到父亲id之后就去data里面遍历
        //看谁的parentId为这个父亲id
        data.forEach(item => {
            if (item.parentId === father_id) {
                //将这个父亲id的所有儿子存到一个数组里面
                array.push(item)
            }
        })
        //再遍历所有儿子
        array.forEach(item => {
            //通过递归传入儿子的对象和对象id,再去找儿子的儿子
            let temp = filter(item,item.id)
            //如果儿子的儿子不为空
            //就将儿子的儿子放进儿子的children对象里面
            if (temp.length != 0) {
                item.children = temp
            }
            //且每个数据都要赋值_parent_对象,值为他们的father对象(也就是传参传进来的)
            item._parent_ = father;
        })
        //然后返回所得数组
        return array
    }
    
    //传入最开始第一层的父亲对象和parentId(第一层没有父亲,所以父亲对象为null 父亲id为0)
    let filterArray = filter(null,0)
    //最后返回过滤完的数据
    return filterArray
}
这样下来,我们每个数据都有他们的父亲对象了也就是_parent_对象了,效果如图:

image.png

我们可以看到id=7,parentId=3的孩子数据中有个_partent_对象,里面存着id=3(和子数据的parentId相同)的数据,显然这就是该孩子数据的父亲了,所以我们就成功啦!

4.又带来了新的问题

你以为又结束了?其实没有。这种方法虽然可以极大的节省时间复杂度,但是他又带来了新的问题!

当你需要对这个树形数组筛选(比如就是按名字搜索数据,也就是通过用户在搜索框输入的名字进行筛选)时,当然你可以让后端写好筛选,你只要发条件就好了,但是如果为了减少请求次数,我们要在前端对这个数据进行过滤的时候,我们就需要把这个数组深拷贝之后(因为筛选不能改变初始数组的值)和要筛选的条件(搜索的条件)传进一个函数然后再处理,那问题出在那里呢?重点来了:

问题就出在这个深拷贝!无论怎么深拷贝肯定是要遍历这个对象的,但是我们这个每个数据都有他的children(最底层没有children)和_parent_对象(第一层没有_parent_对象)的数组其实是个环!所以递归遍历的时候一定会报错,因为他是一个环!他遍历不完,因为可以从children对象往下遍历,也可以从_parent_对象往上遍历,所以递归就没有底了!所以这种方法的缺点就在这里,如果是前面几种方法只有children对象的话就可以自由使用递归遍历深拷贝,但是这种还需要在深拷贝时不让他走_parent_对象这条路:如果是用JSON.parse(JSON.stringify(data))的方法那就要在JSON.stringify中增加一个判断的函数,代码如下:
let filterArray = filterName(JSON.parse(JSON.stringify(data,((key,val) => {
        //判断如果key不等于__parent__才往下递归返回
        if(key != '__parent__')
        return val
  }))),name)
return filterArray;
filterName为筛选函数,name为要筛选的名字条件参数,前面的那一串为深拷贝data之后返回的新数组,作为filterName函数的第一个参数传入,name是第二个参数。
深拷贝之后就在filterName里面写筛选函数就好了,思路就是遍历这个数,然后看数据中的name是否include的传入的name就ok,如果是的则push到数组就ok,然后返回这个数组就是筛选的结果了。

结束: 总的来说,其实方法都有他的好处和坏处,就看你怎么灵活使用了,还是老规矩,如果看到这里,且这个对你有帮助的话,记得点个赞点个关注再走吧,谢谢!