js工具函数,用于把平铺的数组根据对象中的节点id和父节点id转换为树形结构的数组

104 阅读5分钟

背景

后端接口返回包含对象的数组,对象里有节点id,父节点pId。需要前端工程师根据这些信息,拼成树形结构。假设后端接口返回的数据结构如下:

const arr = [
    { id: '1', pId: null, name: '随便1' },
    { id: '1-1', pId: 1, name: '随便1-1' },
    { id: '1-2', pId: 1, name: '随便1-2' },
    { id: '1-1-1', pId: 1-1, name: '随便1-1-1' },
    { id: '1-2-1', pId: 1-2, name: '随便1-2-1' },
    { id: '2', pId: null, name: '随便2' }
]

需要拼成的树状结构如下:

const resultArr = [
    {
        id: '1', 
        pId: null, 
        name: '随便1', 
        children: [
            { 
                id: '1-1', 
                pId: 1, 
                name: '随便1-1', 
                children: [
                    { 
                        id: '1-1-1', 
                        pId: 1-1, 
                        name: '随便1-1-1' 
                    }
                ]
            },
            { 
                id: '1-2', 
                pId: 1, 
                name: '随便1-2',
                children: [
                    { 
                        id: '1-2-1', 
                        pId: 1-2, 
                        name: '随便1-2-1' 
                    }
                ] 
            }
        ]
    },
    { id: '2', pId: null, name: '随便2' }
]

解决方案

我们由于不确定这个数组的层级有多深,所以第一时间想到的是用递归来解决。此处我给出一个不一样的方案给大家。核心步骤如下:

  1. 首先定义一个空数组用于接收拼接好的树形结构数据resultArr,再定义一个空对象mapObj,然后遍历后端返回的数组,mapObj的key是数组对象中的id属性值,mapObj的value是对应的数组对象。mapObj的作用是后续拼接树状结构时,直接在mapObj中对应节点进行拼接.
  2. 再次遍历后端返回的数组,通过数组每一项的id值,在mapObj中找到相应的节点;通过数组每一项的pId值,在mapObj中找到该节点的父节点。判断是否有父节点,如果没有则表明该节点是最外层节点,直接把该节点push到resultArr;如果有父节点,在mapObj中找到对应父节点,然后父节点.children.push(该节点)就可以了。 此时有人会有疑问,不知道数组层级,只循环两次怎么可能实现。下面我会一步一步进行拆解。步骤一中我们定义了一个maObj。代码如下:
const mapObj = {}
arr.forEach(item => {
    mapObj[item.id] = {...item, children: [] }
})

经过步骤一我们得到的mapObj如下:

const mapObj = {
    '1': { id: '1', pId: null, name: '随便1', children:[] },
    '1-1': { id: '1-1', pId: 1, name: '随便1-1', children:[] },
    '1-2': { id: '1-2', pId: 1, name: '随便1-2', children:[] },
    '1-1-1': { id: '1-1-1', pId: 1-1, name: '随便1-1-1', children:[] },
    '1-2-1': { id: '1-2-1', pId: 1-2, name: '随便1-2-1', children:[] },
    '2': { id: '2', pId: null, name: '随便2', children:[] }
}

*下面我们进行步骤二。代码如下:

const resultArr = []
arr.forEach(item => {
    const parentNode = item.pId ? mapObj[item.pId] : null
    const node = mapObj[item.id]
    if (parentNode) {
        parentNode.children.push(node)
    } else {
        resultArr.push(node)
    }
})

步骤二中代码首先进行第一次循环,由于id为'1'的那一项pId为null,所以获取到的parentNode为null,代表此节点是最外层节点没有父节点,那么我们就直接把该节点push到resultArr中。此时mapObj没有变化,resultArr结果如下:

const resultArr = [{ id: '1', pId: null, name: '随便1', children:[] }]

接下来进行第二次循环,此时id为'1-1',pId为'1',那么该节点对应的就是mapObj['1-1']对应的那一项,该节点的父节点对应的就是mapObj['1']那一项。由于有父节点,所以我们把该节点(mapObj['1-1']对应的那一项)push到父节点(mapObj['1']那一项)的children数组里。此时mapObj会发生变化,变化后的结果如下:

const mapObj = {
    '1': { 
        id: '1', 
        pId: null, 
        name: '随便1', 
        children:[
            { 
                id: '1-1', 
                pId: 1, 
                name: '随便1-1', 
                children:[]
            }
        ]
        、、、其余项无变化
}

resultArr变化后的结果如下:

const resultArr = [
    { id: '1', 
      pId: null, 
      name: '随便1', 
      children:[
             { 
                id: '1-1', 
                pId: 1, 
                name: '随便1-1', 
                children:[]
            }
        ] 
     }
 ]

此刻你可能会疑惑,我没有对resultArr里的id为'1'的对象没进行任何操做,为什么resultArr里id为'1'对象的children属性中会多一个id为'1-1'的对象? 由于对象是引用类型,resultArr里的id为'1'的对象和mapObj里id为'1'的对象其实是同一个,我们对mapObj里id为'1'的对象进行操作,resultArr里id为'1'的对象会同步更新。我们正是利用这种引用关系,才会把在mapObj里树形结构同步到resultArr中。 到这里想必你就会理解了,其实resultArr只需要把最外层节点push进来就可以,至于拼接他们的子节点,只需要在mapObj中来完成。由于resultArr中最外层节点其实就是mapObj中的对象,正是由于有这层引用关系,使得我们在mapObj里拼接的树形结构同时,resultArr中的节点也会被同步为树形结构。

封装的函数如下:

/**
 * @param1 {Array} arr: 后端返回的数组数据
 * @param2 {object} options: 由于后端返回字段名不一定,
 * 所以这里支持传自定义字段名,假设后端返回的节点id
 * 字段名是coaId, 父节点id是parentCoaId,那么你的options
 * 应该是{ idKey: 'coaId', pidKey: 'parentCoaId' }
 */
function arrayToTree(arr, options = {}) {
  const { idKey = 'id', pidKey = 'pId' } = options;
  const mapObj = {}; // 哈希表存储节点引用
  const resultArr = []; // 最终树结构

  // 第一次遍历:建立id到节点的映射
  arr.forEach(item => {
    mapObj[item[idKey]] = { ...item, children: [] };
  });

  // 第二次遍历:构建父子关系
  arr.forEach(item => {
    const node = map[item[idKey]];
    const parentNode = item[pidKey] ? map[item[pidKey]] : null;
    
    if (parentNode) {
      parentNode.children.push(node); // 挂载到父节点
    } else {
      resultArr.push(node); // 最外层节点处理
    }
  });

  return resultArr;
}

由于这是我写的第一篇博客,可能有些地方写的不够通俗易懂,请见谅,我会慢慢改进的。后续会把我实际项目中遇到的坑和解决方案陆续更新到掘金中,感谢大家的支持。