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中处理树结构数据就到这里了。当然,树结构数据还有其他的更多操作尚未实现,例如常见的查询子节点列表,节点过滤,最短路径查找等等。但目前列表与树的转换才是最常用的,而且其他操作基本上也是基于它们做的,所以这里也便点到为止了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。