如何将具有父子关系的数据快速变成树结构

3,032 阅读2分钟

在实际项目中,经常会碰到类似的场景,举个例子,很多情况下,数据库中存在父子关系的数据保存在一张表里,而关联关系通过"parentId"属性进行关联,后端在数据查询时,会得到一个数组,此时前端可能需要将数据展示为树状结构,例如一个中后台的权限数据。

  • 很多开发者采用的方式是一遍一遍的遍历数据,比如第一次遍历找到根节点,接着再遍历数据找到根节点的子节点,再遍历数据找到其个子节点的子节点,即使是把已经找到的数据从源数据中剔除后遍历剩余的数据,效率一样非常低,所以强烈不推荐此做法。故而省略代码。

  • 推荐做法1,将数组扁平化,因为每一条数据都有唯一的id,每条数据又是引用类型,故而可以将id和数据关联起来,从而通过扁平的数据结构可以快速获找到对应的数据,而不是遍历大量的数据去找到某一条数据,具体做法如下。

// 数组数据,此处假使具有唯一的根节点
const array = [
    {id: 1, parentId: null, code: '1', text: '1'},
    {id: 2, parentId: 1, code: '2', text: '2'},
    {id: 3, parentId: 1, code: '3', text: '3'},
    {id: 4, parentId: 2, code: '4', text: '4'},
    {id: 5, parentId: 2, code: '5', text: '5'},
    {id: 6, parentId: 3, code: '6', text: '6'},
]

// 第一步,将数组装换成对象,可以将一个具有父子关系的数组转换为一个以id为key的对象
const arrayToObj = (array = []) => {
    const obj = {}
    array.forEach(item => {
        // 避免改变源数据,建议克隆(此处浅克隆)
        obj[`${item.id}`] = Object.assign({}, item)
    })
    return obj
}

const obj = arrayToObj(array)

// 根节点
let root

// 第二步,再遍历一遍数组,从中找到【根节点】以及将子节点放到各自的父节点children属性里
array.forEach(({id, parentId}) => {
    // 从克隆后的对象里面取节点值
    const node = obj[`${id}`]
    
    // 没有parentId则是根节点
    if (!parentId) {
        root = node
    } else if (obj[`${parentId}`]) {
        // children属性保存子节点
        if (!obj[`${parentId}`].children) {
            obj[`${parentId}`].children = []
        }
        obj[`${parentId}`].children.push(node)
    }
})

// 第二遍遍历完成后,根节点即包含所有的子孙节点,总共遍历两次
console.log(root)
  • 推荐做法2,将数组根据parentId分组,然后直接将整个分组挂载到对应的节点
// 数组数据,此处假使具有唯一的根节点
const array = [
    {id: 1, parentId: null, code: '1', text: '1'},
    {id: 2, parentId: 1, code: '2', text: '2'},
    {id: 3, parentId: 1, code: '3', text: '3'},
    {id: 4, parentId: 2, code: '4', text: '4'},
    {id: 5, parentId: 2, code: '5', text: '5'},
    {id: 6, parentId: 3, code: '6', text: '6'},
]

// 避免改变源数据,建议克隆(此处浅克隆)
const arrayCopy = array.map(item => Object.assign({}, item))

// 第一步,将数组根据parentId进行分组,并以parentId为key扁平保存,同时找到根节点
const obj = {} // 保存所有根据parentId分组的子节点
let root
arrayCopy.forEach(item => {
    if (!item.parentId) {
        root = item
        return
    }

    if (!obj[`${item.parentId}`]) {
        obj[`${item.parentId}`] = []
    }
    obj[`${item.parentId}`].push(item)
})

// 第二步,再次遍历数组,挂载自己的子节点
arrayCopy.forEach(item => {
    if (obj[`${item.id}`]) {
        item.children = obj[`${item.id}`]
    }
})

// 第二遍遍历完成后,根节点即包含所有的子孙节点,总共遍历两次
console.log(root)