一天掌握一道手写题-数组生成树

125 阅读2分钟

Why

本题场景为多层菜单渲染,后端返回的数组型数据,前端生成树形结构

  • 后端数据数组,方便在数据库的表结构中存储
  • 前端需要树型结构,因为菜单项往往要封装封装逻辑和样式,需要递归渲染

What

示例:

let arr = [
    {id: 1, name: '菜单1', pid: 0},
    {id: 2, name: '菜单2', pid: 1},
    {id: 3, name: '菜单3', pid: 1},
    {id: 4, name: '菜单4', pid: 3},
    {id: 5, name: '菜单5', pid: 4},
]

转换为

[
    {
        "id": 1,
        "name": "菜单1",
        "pid": 0,
        "children": [
            {
                "id": 2,
                "name": "菜单2",
                "pid": 1,
                "children": []
            },
            {
                "id": 3,
                "name": "菜单3",
                "pid": 1,
                "children": [
                    // ...
                ]
            }
        ]
    }
]

How

直接写法

先来跟着直觉写一下,声明结果数组,for 循环跑起来

const result = []
for(let item of arr) {}

每循环到一个时,都要从 result 里看看有没有父元素,有的话就放进去,顺便加上 children 字段

const result = []
for(let item of arr) {
  for(let child of result) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
    }
  }
}

这时观察数据发现有可能是多层的,所以还要加上 children 的判断,加上后发现循环嵌套了

const result = []
for(let item of arr) {
  // 父节点的循环
  for(let child of result) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
    } else if (child.children.length > 0) {
      // 子节点的循环
      for(let nextchild of child.children) {
        if (nextchild.id === item.pid) {
          nextchild.children.push({...item, children: []})
        } else if (nextchild.children.length > 0) {
          // _(¦3」∠)_
        }
      }

    }
  }
}

要解决嵌套的循环,此时必然掏出酝酿已久的递归,把看起来长得一样的部分提出来一个函数,根据循环里做的事情(给 itemresult 中找到父节点),形象地起一个函数名

// 循环查找函数
function findAFather(item, list) {
  for(let child of list) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
    } else if (child.children.length > 0) {
      findAFather(item, child.children)
    }
  }
}

const result = []
for(let item of arr) {
  findAFather(item, result)
}

OK 大功告成!

等等 Σ(⊙▽⊙"a),如果没找到怎么办?em...

要判断一下查找结果,方法里返回查找结果

function findAFather(item, list) {
  for(let child of list) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
      // 找到后返回 true
      return true
    } else if (child.children.length > 0) {
      findAFather(item, child.children)
    }
  }
  // 没找到返回 false
  return false
}

const result = []
for(let item of arr) {
  findAFather(item, result)
}

每次循环也要把结果带出去

function findAFather(item, list) {
  for(let child of list) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
      return true
    } else if (child.children.length > 0) {
      // 返回下层循环结果
      return findAFather(item, child.children)
    }
  }
  return false
}

const result = []
for(let item of arr) {
  // 收到查找结果
  const nice = findAFather(item, result)
}

没找到的话,他自己也可能是个 father

function findAFather(item, list) {
  for(let child of list) {
    if (child.id === item.pid) {
      child.children.push({...item, children: []})
      return true
    } else if (child.children.length > 0) {
      return findAFather(item, child.children)
    }
  }
  return false
}

const result = []
for(let item of arr) {
  const nice = findAFather(item, result)
  // 没找到就直接添加到结果中
  if (!nice) {
    result.push({...item, children: []})
  }
}

完成!✿✿ヽ(°▽°)ノ✿

Next

优化写法

利用“对象专一的特性”,直接把数组平铺成对象,就可以把复杂的递归查找做成简单的取值查找

let arr = [
  {id: 1, name: '菜单1', pid: 0},
  {id: 2, name: '菜单2', pid: 1},
  {id: 3, name: '菜单3', pid: 1},
  {id: 4, name: '菜单4', pid: 3},
  {id: 5, name: '菜单5', pid: 4},
]

const map = {}
for (let item of arr) {
  map[item.id] = {...item, children: []}
}

const result = []
for(let item of arr) {
  if (map[item.pid]) {
    map[item.pid].children.push(map[item.id])
  } else {
    result.push(map[item.id])
  }
}

console.log(result)