JavaScript 处理树数据结构的方法示例
JavaScript处理树结构数据
场景
即便在前端,也有很多时候需要操作树结构的情况,最典型的场景莫过于无限级分类。之前吾辈曾经遇到过这种场景,但当时没有多想直接手撕JavaScript列表转树了,并没有想到进行封装。后来遇到的场景多了,想到如何封装树结构操作,但考虑到不同场景的树节点结构的不同,就没有继续进行下去了。
直到吾辈开始经常运用了ES6Proxy之后,吾辈想到了新的解决方案!
思考
问:之前为什么停止封装树结构操作了?
答:因为不同的树结构节点可能有不同的结构,例如某个项目的树节点父节点id字段是parent,而另一个项目则是parentId
问:Proxy如何解决这个问题呢?
答:Proxy可以拦截对象的操作,当访问对象不存在的字段时,Proxy能将之代理到已经存在的字段上
问:这点意味着什么?
答:它意味着Proxy能够抹平不同的树节点结构之间的差异!
问:我还是不太明白Proxy怎么用,能举个具体的例子么?
答:当然可以,我现在就让你看看Proxy的能力
下面思考一下如何在同一个函数中处理这两种树节点结构
/**
*系统菜单
*/
classSysMenu{
/**
*构造函数
*@param{Number}id菜单id
*@param{String}name显示的名称
*@param{Number}parent父级菜单id
*/
constructor(id,name,parent){
this.id=id
this.name=name
this.parent=parent
}
}
/**
*系统权限
*/
classSysPermission{
/**
*构造函数
*@param{String}uid系统唯一uuid
*@param{String}label显示的菜单名
*@param{String}parentId父级权限uid
*/
constructor(uid,label,parentId){
this.uid=uid
this.label=label
this.parentId=parentId
}
}
下面让我们使用Proxy来抹平访问它们之间的差异
constsysMenuMap=newMap().set('parentId','parent')
constsysMenu=newProxy(newSysMenu(1,'rx',0),{
get(_,k){
if(sysMenuMap.has(k)){
returnReflect.get(_,sysMenuMap.get(k))
}
returnReflect.get(_,k)
},
})
console.log(sysMenu.id,sysMenu.name,sysMenu.parentId)//1'rx'0
constsysPermissionMap=newMap().set('id','uid').set('name','label')
constsysPermission=newProxy(newSysPermission(1,'rx',0),{
get(_,k){
if(sysPermissionMap.has(k)){
returnReflect.get(_,sysPermissionMap.get(k))
}
returnReflect.get(_,k)
},
})
console.log(sysPermission.id,sysPermission.name,sysPermission.parentId)//1'rx'0
定义桥接函数
现在,差异确实抹平了,我们可以通过访问相同的属性来获取到不同结构对象的值!然而,每个对象都写一次代理终究有点麻烦,所以我们实现一个通用函数用以包装。
/**
*桥接对象不存在的字段
*@param{Object}map代理的字段映射Map
*@returns{Function}转换一个对象为代理对象
*/
exportfunctionbridge(map){
/**
*为对象添加代理的函数
*@param{Object}obj任何对象
*@returns{Proxy}代理后的对象
*/
returnfunction(obj){
returnnewProxy(obj,{
get(target,k){
if(Reflect.has(map,k)){
returnReflect.get(target,Reflect.get(map,k))
}
returnReflect.get(target,k)
},
set(target,k,v){
if(Reflect.has(map,k)){
Reflect.set(target,Reflect.get(map,k),v)
returntrue
}
Reflect.set(target,k,v)
returntrue
},
})
}
}
现在,我们可以用更简单的方式来做代理了。
constsysMenu=bridge({
parentId:'parent',
})(newSysMenu(1,'rx',0))
console.log(sysMenu.id,sysMenu.name,sysMenu.parentId)//1'rx'0
constsysPermission=bridge({
id:'uid',
name:'label',
})(newSysPermission(1,'rx',0))
console.log(sysPermission.id,sysPermission.name,sysPermission.parentId)//1'rx'0
定义标准树结构
想要抹平差异,我们至少还需要一个标准的树结构,告诉别人我们需要什么样的树节点数据结构,以便于在之后处理树节点的函数中统一使用。
/**
*基本的Node节点结构定义接口
*@interface
*/
exportclassINode{
/**
*构造函数
*@param{Object}[options]可选项参数
*@param{String}[options.id]树结点的id属性名
*@param{String}[options.parentId]树结点的父节点id属性名
*@param{String}[options.child]树结点的子节点数组属性名
*@param{String}[options.path]树结点的全路径属性名
*@param{Array.
实现列表转树
列表转树,除了递归之外,也可以使用循环实现,这里便以循环为示例。
思路
- 在外层遍历子节点
- 如果是根节点,就添加到根节点中并不在找其父节点。
- 否则在内层循环中找该节点的父节点,找到之后将子节点追加到父节点的子节点列表中,然后结束本次内层循环。
/**
*将列表转换为树节点
*注:该函数默认树的根节点只有一个,如果有多个,则返回一个数组
*@param{Array.
抽取通用的树结构遍历逻辑
首先,明确一点,树结构的完全遍历是通用的,大致实现基本如下
- 遍历顶级树节点
- 遍历树节点的子节点列表
- 递归调用函数并传入子节点
/**
*返回第一个参数的函数
*注:一般可以当作返回参数自身的函数,如果你只关注第一个参数的话
*@param{Object}obj任何对象
*@returns{Object}传入的第一个参数
*/
exportfunctionreturnItself(obj){
returnobj
}
/**
*遍历并映射一棵树的每个节点
*@param{Object}root树节点
*@param{Object}[options]其他选项
*@param{Function}[options.before=returnItself]遍历子节点之前的操作。默认返回自身
*@param{Function}[options.after=returnItself]遍历子节点之后的操作。默认返回自身
*@param{Function}[options.paramFn=(node,args)=>[]]递归的参数生成函数。默认返回一个空数组
*@returns{INode}递归遍历后的树节点
*/
exportfunctiontreeMapping(
root,
{
before=returnItself,
after=returnItself,
paramFn=(node,...args)=>[],
}={},
){
/**
*遍历一颗完整的树
*@param{INode}node要遍历的树节点
*@param{...Object}[args]每次递归遍历时的参数
*/
function_treeMapping(node,...args){
//之前的操作
let_node=before(node,...args)
constchilds=_node.child
if(arrayValidator.isEmpty(childs)){
return_node
}
//产生一个参数
constlen=childs.length
for(leti=0;i
使用treeMapping遍历树并打印
consttree={
uid:1,
childrens:[
{
uid:2,
parent:1,
childrens:[{uid:3,parent:2},{uid:4,parent:2}],
},
{
uid:5,
parent:1,
childrens:[{uid:6,parent:5},{uid:7,parent:5}],
},
],
}
//桥接函数
constbridge=bridge({
id:'uid',
parentId:'parent',
child:'childrens',
})
treeMapping(tree,{
//进行桥接抹平差异
before:bridge,
//之后打印每一个
after(node){
console.log(node)
},
})
实现树转列表
当然,我们亦可使用treeMapping简单的实现treeToList,当然,这里考虑了是否计算全路径,毕竟还是要考虑性能的!
/**
*将树节点转为树节点列表
*@param{Object}root树节点
*@param{Object}[options]其他选项
*@param{Boolean}[options.calcPath=false]是否计算节点全路径,默认为false
*@param{Function}[options.bridge=returnItself]桥接函数,默认返回自身
*@returns{Array.
现在,我们可以转换任意树结构为列表了
consttree={
uid:1,
childrens:[
{
uid:2,
parent:1,
childrens:[{uid:3,parent:2},{uid:4,parent:2}],
},
{
uid:5,
parent:1,
childrens:[{uid:6,parent:5},{uid:7,parent:5}],
},
],
}
constfn=bridge({
id:'uid',
parentId:'parent',
child:'childrens',
})
constlist=treeToList(tree,{
bridge:fn,
})
console.log(list)
总结
那么,JavaScript中处理树结构数据就到这里了。当然,树结构数据还有其他的更多操作尚未实现,例如常见的查询子节点列表,节点过滤,最短路径查找等等。但目前列表与树的转换才是最常用的,而且其他操作基本上也是基于它们做的,所以这里也便点到为止了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。