树形数据在平常业务中出现的频率很高。在做动态路由渲染时,或者是某些组织架构列表时,会需要这种父子关系层层嵌套的树状数据。
然后很多时候,后端传输过来的数据结构确实扁平化的。意思是,在一个数组里一元摆放所有数据组,他们之间的关系通过某些特征码来进行耦合。前端在拿到数据之后,要通过一定的规则,来将它转化为合适的树状解构数据。
const data = [
{
name: father01,
id: 001,
pid: 000,
path: '/father01',
},
{
name: father02,
id: 002,
pid: 000,
path: '/father02',
},
{
name: tom,
id: 003,
pid: 001,
path: '/tom',
},
{
name: bob,
id: 004,
pid: 002,
path: '/bob',
},
{
name: sofia,
id: 005,
pid: 004,
path: '/sofia',
},
]
数组中所有的对象都有id和pid两个属性,id是唯一的,pid有时会和某些id呈对应关系,表示该对象为对应id对象的子数据。pid为000则表示该对象为顶级数据,不存在父对象。
这是处理后需要的结构
const data = [
{
name: father01,
id: 001,
pid: 000,
path: '/father01',
children: [
{
name: tom,
id: 003,
pid: 001,
path: '/tom',
}
]
},
{
name: father02,
id: 002,
pid: 000,
path: '/father02',
children: [
{
name: bob,
id: 004,
pid: 002,
path: '/bob',
children: [
{
name: sofia,
id: 005,
pid: 004,
path: '/sofia',
}
]
}
]
}
]
通常有两种方法可以转化这样的数据,一种采用递归,另一种只采用遍历。
递归转化树状结构数据
递归处理数据,是转换树状结构数据的通用方法,也是最符合人思考逻辑的一种方法。它的代码通俗易懂,循环渐进。而且根据不同的业务要求可以相应修改实现细节。
// step.1 先filter过滤原数组,将顶级元素和子集元素分离出来。
function formatDataTree(data) {
const parents = data.filter(el => el.pid === 000),
children = data.filter(el => el.pid !== 000)
formatToTree(parents, children)
return parents
// step.2 循环处理父级与子集的嵌套
function formatToTree(parents, children) {
parents.map(p =>{
children.map(c => {
// child 的 pid 与 parent 的 id 相等,是该parent的子数据
if (c.pid === p.id) {
// parent 是否有children属性,分别做判断
if (p.children) {
p.children.push(c)
} else {
p.children = [c]
}
}
})
})
}
}
然而目前并没有完成整个数据的处理,只是对第一层的children进行了设置,若child数据还有children属性的话,上面的代码并没有关注它。
这时就需要对所有的子数据进行递归处理了,递归这个formatToTree函数
function formatToTree(parents, children) {
parents.map(p =>{
children.map((c, index) => {
if (c.pid === p.id) {
/*
* 当一个子元素匹配到父元素,且在将自己加入到父元素的children数组中之
前
* 需要处理子元素里的子元素
* 需要注意的是,每当为一个子元素进行递归处理时,递归函数第二个参数不
应当
永远为包含所有children的数组
* 所以,当递归处理某个子元素时,应该将其从children数组中剔除,那么当
前及
后面所有子元素递归时都不在会处理这个子元素(因为这个业务没有对应多个
父元素)
* 直接从原children中剔除也不合适,最佳方法是深拷贝一个原children的
复制,
对它进行相关的操作
**/
let _children = JSON.parse(JSON.stringify(children))
// 复制的children中剔除当前的子元素
_children.splice(index, 1)
// 递归的函数第一个参数需要一个数组,所有需要给当前子元素包装一下
formatToTree([c], _children)
if (p.children) {
p.children.push(c)
} else {
p.children = [c]
}
}
})
})
}
}
扁平化处理方式
还有一种代码量急剧减少但是理解难度陡增的方法。该方法采用扁平化的处理,不需要递归数据。
function formatDataTree (data) {
// 深拷贝一个原数据的副本
let _data =JSON.parse(JSON.stringify(data))
// 使用filter方法来处理拷贝的副本
return _data.filter(p => {
/*
* 在处理每一项子数据时再次filter副本数据
* 找出所有pid有与之对应id的数据,暂存在_arr变量中
* 判断_arr不为空,说明当前filter的数据拥有子数据,那么创建children属性,
值为_arr
**/
const _arr = _data.filter(c => c.pid === p.id)
_arr.length && (p.children = _arr)
// 最后将所有pid为0,这里可以是任何你定义的顶级数据的pid,返回出方法
return p.pid === 0
})
}