js中操作树结构的数据

现在前端操作树的数据结构还是挺常见的,我在这里总结一下js树形结构常见的操作方式。我也把它做成了工具类库,github地址

一.遍历树结构的方式

假设现在有如下的数据结构,我们需要去找到树结构的中某个对象的数据,常见的操作方式应该是用递归进行遍历查找,我认为这是最基本应该能想到的。

let tree = [
  {
    id: '1',
    title: '节点1',
    children: [
      {
        id: '1-1',
        title: '节点1-1'
      },
      {
        id: '1-2',
        title: '节点1-2'
      }
    ]
  },
  {
    id: '2',
    title: '节点2',
    children: [
      {
        id: '2-1',
        title: '节点2-1'
      }
    ]
  }
]
const recursion = (cityData, id) => {
      if (!cityData || !cityData.length) {
        return
      }
      //先循坏cityData
      for (let i = 0; i < cityData.length; i++) {
        const childs = cityData[i].children;
        // console.log(cityData[i].id)
        if (cityData[i].id === id) {
          result = cityData[i]
        }
        if (childs && childs.length > 0) {
          recursion(childs, id);
        }
      }
      return result
    }
    console.log(recursion(cityData, "2-1"))// {title:"节点2-1",id:"2-1"}

也可以使用栈迭代的方式,这也是我个人平时遍历树常用的一种方式。

let result = ''

const range = (cityData, id) => {
  if (!cityData || !cityData.length) return;
  // 定义一个数据栈
  let stack = [];

  let item = null;

  //先将第一层节点放入栈
  for (var i = 0, len = cityData.length; i < len; i++) {
    stack.push(cityData[i]);
  }

  while (stack.length) {
    // 将数据栈的第一个取出来
    item = stack.shift();
    // 如果符合就赋值给result
    if (item.id === id) {
      result = item
    }
    //如果该节点有子节点,继续添加进入栈底
    if (item.children && item.children.length) {
      stack = stack.concat(item.children);
    }
  }
  return result
};

console.log(range(cityData, "2-1"))// {title:"节点2-1",id:"2-1"}

二.操作树的增删改查

1.列表与树的相互装换
列表的数据结构会在节点信息中给定当前父元素的pid,然后可以通过pid进行相互关联进而转换成为一棵树结构。

let list = [
      {id: "1",title: "节点1",parentId: "",},
      {id: "1-1",title: "节点1-1",parentId: "1"},
      {id: "2",title: "节点2",parentId: ""},
      {id: "2-1", title: "节点2-1", parentId: "2"},
      {id: "1-2", title: "节点1-2", parentId: "1"},
      {id: "1-3", title: "节点1-3", parentId: "1"},
      {id: "2-3", title: "节点2-3", parentId: "2"},
      {title: "节点2-3-1", parentId: "2-3",id: "2-3-1"}
    ];

在tree.js中

class Tree {

  constructor(config = {}) {
    this.defaultConfig = {
      id: "id",
      children: "children",
      pid: "pid"
    };
    this.config = Object.assign(this.defaultConfig, config);
  }

  // list=>tree 列表转换成为树
  listToTree(list) {
    let info = list.reduce((map, node) => {
      if (!map[node[this.config.id]]) {
        map[node[this.config.id]] = node;
        node.children = [];
      }
      return map;
    }, {});
    return list.filter(v => {
      if (info[v[this.config.pId]]) {
        info[v[this.config.pId]].children.push(v);
      }
      return !v[[this.config.pId]];
    });
  }
}

export default Tree;

这里我们利用了对象key的唯一性,把info与当前元素id建立起来一个映射关系,那么它映射出来的结构如下:
在这里插入图片描述
然后通过filter遍历list中的元素,判断当前当前元素的pid是否存在于info集合中,如果存在,则说明当前元素为info中某项的子元素。然后再过滤出pid为空的元素。

调用:

let list = [
      {
        id: "1",
        title: "节点1",
        parentId: "",
      },
      {
        id: "1-1",
        title: "节点1-1",
        parentId: "1"
      },

      {
        id: "2",
        title: "节点2",
        parentId: ""
      },
      {id: "2-1", title: "节点2-1", parentId: "2"},
      {id: "1-2", title: "节点1-2", parentId: "1"},
      {id: "1-3", title: "节点1-3", parentId: "1"
      },
      {id: "2-3", title: "节点2-3", parentId: "2"},
      {title: "节点2-3-1", parentId: "2-3", id: "2-3-1"}
    ];
    console.log(new Tree({id: "id", pId: "parentId", children: "children"}).listToTree(list))

2.树转换成为列表

treeToList(tree) {
    const { children } = this.config; const result = [...tree];
    for (let i = 0; i < result.length; i++) {
      if (!result[i][children]) {continue;}
      result.splice(i + 1, 0, ...result[i][children]);
    }
    return result;
  }

树变成列表你可以这样理解,遍历当前项数据,判断children.length是否大于0,如果大于则把它的children中的每个元素都排进列表中即可,由于此时result长度发生了变化,那么它的循环也会继续走下去。如果children.length === 0 则说明他没有子节点则不需要任何的操作。

3.删除树节点中的某个数据
其实想要删除某个节点我们只需要找到它的父节点,利用父节点的children属性再进行splice即可,需要注意的一点就是当pid:0的时候,它的父节点就是当前的树,那么我们需要一个函数getData通过它的id寻找出当前的元素,再通过它的pid去找到它的父元素。

//获取某个id的元素
getData(tree, id) {
    let stack = [];
    let result = {};
    if (Array.isArray(tree) && tree.length > 0) {
      // tree.
      for (let i = 0;i < tree.length;i++) {
        stack.push(tree[i]);
      }
    } else if (typeof tree === "object") {
      stack = [tree];
    }
    while (stack.length) {
      let node = stack.shift();
      if (node[this.config.id] === id) {
        result = node;
        return result;
      }
      if (node[this.config.children] && node[this.config.children].length > 0) {
        stack = stack.concat(node[this.config.children]);
      }
    }
    return result;
  },
  // 删除某个节点
  removeNode(tree, id) {
    let currentNode = this.getData(tree, id);
    if (Object.keys(currentNode).length > 0) {
      let parentNode = currentNode[this.config.pId] ? this.getData(tree, currentNode[this.config.pId]) : tree;
      let parent = parentNode[this.config.children] ? parentNode[this.config.children] : parentNode;
      let currentIndex = parent.findIndex(v => v[this.config.id] === id);
      currentIndex > -1 && parent.splice(currentIndex, 1);
    }
  }

4.插入某个子节点
同理也需要找到当前元素通过children进行push操作

// 插入某个子节点
  insertChildrenNode(tree, pId, node) {
    if (pId) { // 说明的时候某个节点
      let currentNode = this.getData(tree, pId);
      if (Object.keys(currentNode).length > 0) {
        if (!currentNode.children) {
          currentNode.children = [];
        }
        currentNode.children.push(node);
      }
    } else if (pId === "" || pId === 0 || pId === "0") { // 说明操作的根节点
      tree.push(node);
    }
  }

5.插入某个节点之后

insertAfter (tree, sourceId, targetNode) {
    let pNode = this.findIdParentNode(tree, sourceId); // 找出它的父节点
    let sIndex = pNode.findIndex(v => v[this.config.id] === sourceId);
    console.log(pNode);
    if (sIndex > -1) {
      pNode.splice(sIndex + 1, 0, targetNode);
    }
  }

调用

let cnode = {
      parentId: "",
      id: "3",
      children: [],
      title: "3"
    };
    let c1node = {
      parentId: "",
      id: "4",
      children: [],
      title: "34"
    };
    treeObj.insertAfter(treeData, "1", cnode);
    treeObj.insertAfter(treeData, "3", c1node);
    console.log(treeData);

同理也是通过findParentNode()找出它的父节点,通过splice直接操作添加即可。

6.通过某个节点向上找寻所有的节点

const list = [
      {
        id: '1',
        title: '节点1',
        parentId: ''
      },
      {
        id: '1-1',
        title: '节点1-1',
        parentId: '1'
      },
      {
        id: '1-1-1',
        title: '节点1-1-1',
        parentId: '1-1'
      },
      {
        id: '1-2',
        title: '节点1-2',
        parentId: '1'
      },
      {
        id: '2',
        title: '节点2',
        parentId: ''
      },
      {
        id: '2-1',
        title: '节点2-1',
        parentId: '2'
      }
    ]
    function listToTree(list) {
      const config = {
        id: 'id',
        children: 'children',
        pid: 'parentId'
      }
      const nodeMap = new Map()
      const result = []
      const { id, children, pid } = config
      for (const node of list) {
        node[children] = node[children] || []
        nodeMap.set(node[id], node)
      }
      for (const node of list) {
        const parent = nodeMap.get(node[pid])
        if (parent) {
          parent.children.push(node)
        } else {
          result.push(node)
        }
      }
      return result
    }
function findPath(tree, func) {
      const config = {
        id: 'id',
        children: 'children',
        pid: 'parentId'
      }
      const path = []
      const list = [...tree]
      const visitedSet = new Set()
      const { children } = config
      while (list.length) {
        const node = list[0]
        if (visitedSet.has(node)) {
          path.pop()
          list.shift()
        } else {
          visitedSet.add(node)
          node[children] && list.unshift(...node[children])
          path.push(node)
      
          
          if (func(node)) { return path }
        }
      }
      return null
    }
    const callback = node => node.id === '2-1'
    console.log(findPath(listToTree(list), callback).map(v => v.id), 'awwww')

findPath函数,原理也是比较简单,通过栈迭代,找寻符合规则的节点,通过set来记录起已经遍历过的节点,如果该节点满足规则,则会直接返回节点路径,如果不满足,则会删除path和list的数据。

也可以使用递归+回溯的方式寻找叶子节点路径

let res = []
      function dfs(list,path){
        if(list.length === 0){
          return 
        }
        for(let i = 0;i<list.length;i++){
          let target = list[i]
          path.push(target.id)

          if(target.children &&target.children.length>0){
            dfs(target.children,path)
          } else {
            res.push([...path])
          }
          path.pop()
        }
      }
      dfs(list,[])
      return res

可以获取所有叶子节点路径。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值