背景
后端接口返回包含对象的数组,对象里有节点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' }
]
解决方案
我们由于不确定这个数组的层级有多深,所以第一时间想到的是用递归来解决。此处我给出一个不一样的方案给大家。核心步骤如下:
- 首先定义一个空数组用于接收拼接好的树形结构数据resultArr,再定义一个空对象mapObj,然后遍历后端返回的数组,mapObj的key是数组对象中的id属性值,mapObj的value是对应的数组对象。mapObj的作用是后续拼接树状结构时,直接在mapObj中对应节点进行拼接.
- 再次遍历后端返回的数组,通过数组每一项的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;
}
由于这是我写的第一篇博客,可能有些地方写的不够通俗易懂,请见谅,我会慢慢改进的。后续会把我实际项目中遇到的坑和解决方案陆续更新到掘金中,感谢大家的支持。