扁平化数据的树状渲染处理

330 阅读4分钟

树形数据在平常业务中出现的频率很高。在做动态路由渲染时,或者是某些组织架构列表时,会需要这种父子关系层层嵌套的树状数据。

然后很多时候,后端传输过来的数据结构确实扁平化的。意思是,在一个数组里一元摆放所有数据组,他们之间的关系通过某些特征码来进行耦合。前端在拿到数据之后,要通过一定的规则,来将它转化为合适的树状解构数据。

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
  })
}