当前位置: 首页 > Web前端 > JavaScript

从列表生成树(JavaScript-TypeScript)

时间:2023-03-26 22:49:41 JavaScript

大多数情况下,树展示从服务器获取的数据是扁平的,即列表。这是因为关系型数据库是以“行”为单位保存数据的,所以它保存的是每个节点的数据,而这个数据中包含了它和父节点的关系(比如parentId)。前端要将这样的列表数据以树的形式展示出来,需要将列表数据转换为树结构数据。这种树结构意味着每个节点数据都包含其子节点集(通常是children属性)。因此,树形节点的数据结构主要需要包含如下信息(TypeScript数据结构声明):interfaceTreeNodeBase{id:TKey;parentId:TKeychildren?:TreeNodeBase[]}这里使用TypeScript的通用语法来描述树节点结构。从自然语义上不难理解:树节点的id(包括parentId)是string或number类型,极少数情况下也可能是混合类型。树节点包含一个parentId。由于这个parentId不是可选的(没有用?声明),所以根节点通常使用一个特殊的值,或者一个不应该存在的ID值,比如0(如果是数据库自增字段,一般会从1)树节点包含一组可选的子节点,children,其每个元素与当前元素的类型相同。这个类型参数的作用是约束子节点的类型与父节点保持一致,避免父节点节点的id是string类型,但是子节点的id做成了字符串.(不包括混合类型id的情况。)科普完树节点的数据结构,再来说说转换过程。一般来说,转换可能分三个阶段进行:后端发出前,前端处理后再发出,然后前端自己转换,然后转换后的数组用于呈现页面。前端用到的UI组件都有自己的转换功能,不用开发者担心(比如zTree)这里我就以JS/TS为例说说如何转换。语言不重要,重要的是怎么想,用什么方法转换。这里同时使用JS和TS的原因是:TypedTS可以清晰的描述数据结构,而JS代码对大多数人来说可能看起来压力较小。1.准备样本数据(随机生成)用列表表示的树数据,每一个(行)必须明确描述这个节点的三个元素:自我标识(ID),通常带有id,key,name等属性它可以唯一标识一个节点与其父节点之间的关系。通过使用parentId、upstreamId等名称,可以明确表示其父节点自身携带的数据,如显示的文本、标题、标签等,以及其他一些数据。为了快速准备样本数据,我们使用最简单的数据结构,属性含义明确。请注意,此结构是扁平的以匹配数据表,没有子子集。//TypeScript接口TreeNode{id:number;parentId:编号;label:string;}然后写一段代码随机生成数据。在此之前,我们约定有效节点的id从1开始,如果一个节点的parentId===0,则表示该节点没有父节点。思路:循环生成一组节点,每个节点的id为序号+1(序号从0开始)//JavaScriptconstnodes=[];计数节点Count=20;for(leti=0;i{return[...`${chars}${chars.toLowerCase()}${nums}`];})("ABCDEFGHIJKLMNOPQRSTUVWXYZ","0123456789");其实直接给一个包含所有字符的字符串就可以了,但是不想再写a~z,所以用了一个IIFE来复用A~Z。另外需要注意的是:字符串本身可以使用[]索引操作符来取字符,所以也可以不事先转成字符数组。接下来是随机生成字符串。根据长度随机选择n个字符,然后加入://JavaScriptfunctionrandomString(length){returnArray.from(Array(length),()=>CHARS[randomInt(CHARS.length)]).join("");}randomString()可以生成指定长度的随机字符串。这里Array(length)将生成一个长度为length但没有元素的数组,您可以使用Array.from()将其转换为具有元素的数组(默认为未定义)。转换时,Array.from()的第二个参数是映射函数,与Array.prototype.map()的参数相同。现在,我们可以继续改进node.push(...)中的标签部分:{id:i+1,parentId:randomInt(i+1),label:randomString(5+randomInt(10))//lengthin[5,15)randomstringsintheinterval}至此,准备样本数据的关键代码已经有了。这是一个完整的//TypeScriptinterfaceTreeNode{id:number;parentId:编号;标签:字符串;}constCHARS=((chars,nums)=>{return[...`${chars}${chars.toLowerCase()}${nums}`];})("ABCDEFGHIJKLMNOPQRSTUVWXYZ","0123456789");functionrandomInt(max:number):number{returnMath.floor(Math.random()*max);}functionrandomString(length:number=10):string{returnArray.from(数组(length),()=>CHARS[randomInt(CHARS.length)]).join("");}函数randomTreeNodes(count:number=20):TreeNode[]{return[...Array(count).keys()].map(i=>({id:i+1,parentId:randomInt(i+1),//从生成的节点中随机找一个label:randomString(5+randomInt(10))//随机生成一个长度of[5,15)string}));}完整的代码是用TypeScript写的。如果需要JavaScript,可以根据上面每一步的关键代码拼出来。或者获取TypeScript代码并在TypeScriptPlayground中转换它。最后,我们可以直接调用randomTreeNodes()来生成我们需要的树结构://JavaScript|TypeScriptconsttreeNodes=randomTreeNodes();得到的treeNodes将作为后面treedemo代码的数据源。以下是其中一次运行的结果:[{id:1,parentId:0,label:'8WUg35y'},{id:2,parentId:1,label:'Pms1S5Mx'},{id:3,parentId:1,标签:'RUTKSF'},{id:4,parentId:1,标签:'IYkxXlhmU12x'},{id:5,parentId:4,标签:'p2Luabg9mK2'},{id:6,parentId:0,label:'P6mtcgfCD'},{id:7,parentId:1,label:'yluJgpnqKthR'},{id:8,parentId:6,label:'m6o5UsytQ0'},{id:9,parentId:2,标签:'glcR5yGx'},{id:10,parentId:0,标签:'lhDGTNeeSxLNJ'},{id:11,parentId:1,标签:'r7ClxBCQS6'},{id:12,parentId:7,标签:'5W6vy0EuvOjN'},{id:13,parentId:5,label:'LbpWq'},{id:14,parentId:6,label:'ysYwG8EFLAu1a'},{id:15,parentId:8,label:'R2PmAh1'},{id:16,parentId:10,label:'RKuQs4ki65wo'},{id:17,parentId:10,label:'YN88ixWO1PY7f4'},{id:18,parentId:13,label:'03X6e4UT'},{id:19,parentId:7,label:'LTJTeF'},{id:20,parentId:19,label:'3rqUqE3MLShh'}]如果用图形表示:flowchartLR%%{init:{"theme":"forest"}}%%S(("Virtual\nRoot"))-->N1S-->N6S-->N10N1("1|8WUg35y")-->N2(“2|Pms1S5Mx”)N1-->N3(“3|RUTKSF”)N1-->N4(“4|IYkxXlhmU12x”)N4-->N5(“5|p2Luabg9mK2”)N6(“6|P6mtcgfCD")N1-->N7("7|yluJgpnqKthR")N6-->N8("8|m6o5UsytQ0")N2-->N9("9|glcR5yGx")N10("10|lhDGTNeeSxLNJ")N1-->>N11(“11|r7ClxBCQS6”)N7-->N12(“12|5W6vy0EuvOjN”)N5-->N13(“13|LbpWq”)N6-->N14(“14|ysYwG8EFLAu1a”)N8-->N15(“15|R2PmAh1”)N10-->N16(“16|RKuQs4ki65wo”)N10-->N17(“17|YN88ixWO1PY7f4”)N13-->N18(“18|03X6e4UT”)N7-->N19(“19|LTJTeF")N19-->N20("20|3rqUqE3MLShh")美人鱼是个好东西,要不要支持一下!2.由demo数据生成一棵树在idea完全形成之前,拿起键盘开始敲代码——这种行为一般算作“实验”,但即使是实验,也应该先理顺你的想法。目前已知每个节点都已经包含了关键数据:用于标识节点的id,以及用于标识其父关系的parentId。那么,在处理一个节点时只需要根据其parentId找到其父节点,将当前节点添加到父节点的children[]数组中,即可生成树结构数据。这里有几个相关的问题需要考虑:由于一个节点只有一个parentId,所以最后只会加到某个节点的children[]中,不可能出现在多个节点的children[]中;对于没有parentId或parentId值为0的节点,认为是根节点。但它可能不是唯一的根节点,所以我们需要一个额外的roots[]数组来保存所有的根节点。思考:如何根据parentId找到对应的节点数据?前两题很好理解,第三题需要算法思考。由于节点列表中有父节点,可以直接使用parentId在节点列表中查找//JavaScriptconstparentNode=treeNodes.find(node=>node.id===parentId);在遍历和处理节点的过程中,根据上面的生成数据的逻辑可以得出当前节点的父节点一定在它之前。如果知道父节点和子节点的关系比较密切,可以优化为倒序查找。这个过程可以定义为一个函数findParent()://JavaScript/***@paramid要查找的父节点id*@paramindex当前节点在treeNodes中的序号*/constfindParent=(id,index)=>{for(leti=index-1;i>=0;i--){if(treeNodes[i].id===id){returntreeNodes[i];}}};事实上,大多数情况下并不清楚是找到离起始位置更近的父节点还是离子节点,所以根本不需要写反向查找,直接使用Array.prototype.find()即可。找到父节点后,在将当前节点节点添加到parentNode子节点集之前,要特别注意其子节点集是否存在。可以使用Logicalnullishassignment(??=)operator来简化代码,一句话就可以搞定:(parentNode.children??=[]).push(node);这里还有一个性能相关的问题。在数据量很大的情况下,不管是顺序还是逆序搜索,都可能会扫描到很多节点,使用Map可以大大提高搜索效率。当节点有序时(即父节点必须在前),可以边遍历边生成Map。比较完整的代码示例://JavaScriptfunctionmakeTree(treeNodes){constnodesMap=newMap();常量根=[];treeNodes.forEach((node,i)=>{nodesMap.set(node.id,node);if(!node.parentId){roots.push(node);return;}constparent=nodesMap.get(node.parentId);(parent.children??=[]).push(node);});returnroots;}如果将上面的JavaScript代码改成TypeScript代码,在添加类型声明的时候还是会出现问题:靠近尾部的parent.children会将parent标记为红色,并报“Objectispossibly'undefined'”。这个问题说明根据parentId在Map中查找父节点时,有可能找不到。在这个例子中,由于我们生成treeNodes的代码可以保证被找到,所以我们可以忽略编译器的顾虑,直接改成parent!.children来隐藏这个风险提示。但是后台传来的真实数据并不能保证nodesMap.get(node.parentId)不会返回undefined。至少有两种情况会导致这个问题:节点的顺序不是先父后子的顺序(多是在移动节点之后)。在这种情况下,由于父节点在子节点之后,所以在添加到Map之前必须从中查找,当然找不到了。解决这个问题,只需要提交遍历所有节点,生成一个完整的Map。由于后端错误或业务需要,无法将所有节点发回。除了报错,前端还有两种容错处理方式:丢弃没有找到父节点的节点。这种容错不难处理,就不多说了。把没有父节点的节点当作根节点——既然数据来了,大多数情况下都会采用这种容错方式。接下来就是添加makeTree进行容错处理了。3.从列表生成树的完整TypeScript代码interfaceTreeNode{id:number;parentId:编号;标签:字符串;children?:TreeNode[]}functionmakeTree(treeNodes:TreeNode[]):TreeNode[]{//预先生成节点查找表。//如果明确节点的顺序可以保证先父后子的顺序,则本次遍历可以省略,在后面的遍历过程中填充查找表constnodesMap=newMap(treeNodes.map(node=>[node.id,node]));//引入一个虚拟根节点,统一实现父节点永远有效,避免空判断constvirtualRoot={}asPartial;treeNodes.forEach((node,i)=>{constparent=nodesMap.get(node.parentId)??virtualRoot;(parent.children??=[]).push(node);});返回virtualRoot.children??[];}是的,这段代码不长。但是,有没有Get到渐进式分析处理的过程呢?TypeScript的类型检查是否让你印象深刻?来我的课:TypeScript从入门到实践【2021版】——思考编程,深入理解TypeScript的语言特性,写出高质量的代码,掌握Vue前端和Koa后端技术的应用基础TypeScript,掌握前后端分离的开发模式、设计模式和发布方式将类型系统融入编程思维,提高理解和设计能力

最新推荐
猜你喜欢