现在前端操作树的数据结构还是挺常见的,我在这里总结一下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
可以获取所有叶子节点路径。