前言在用户体验方面,下面的控件源和-design给出了一个带有输入框控件的树控件,可以模糊查询treedata查询完数据后,展开相关节点。一般来说,它是一个可查询的树形组件,所以这应该是我们中后台业务中不可或缺的业务。那么让我们来看看问题是什么。如果给一个树控件,然后给1w条数据,会怎样,3k条数据会怎样。首先想到的是数据量大,肯定会影响渲染时间,有什么好的解决方案?您可以在下面分享并写下我的解决方案。由于本文是参与GoldstoneProject,如果您觉得本文对您有点帮助,请点赞支持。测试数据首先我们测试10000多条数据。通过模糊查询和展开相关节点需要多长时间?查看测试数据,对测试数据有更深的理解1wtest.js包含了10000多条测试数据。下面是我把数据压扁后的片数。测试数据代码结构共有18402条。看一下整体代码结构html结构-1">{{label.substr(0,label.indexOf(searchValue)))}}{{searchValue}}{{标签。substr(label.indexOf(searchValue)+searchValue.length)}}{{label}}
javascript结构示例代码和-设计示例先基于and-design实现,直接拿官网示例代码,替换数据,替换为上面的10000多条测试数据从'ant-design-vue/es/tree/Tree'导入{TreeDataItem};constdataList:TreeDataItem[]=[];constgenerateList=(data:TreeDataItem[])=>{for(leti=0;i{让parentKey;for(leti=0;iitem.id===id)){parentKey=node.id;}elseif(getParentKey(id,node.children)){parentKey=getParentKey(id,node.children);}}}返回parentKey;};constsearch=()=>{//记录模糊查询和dom扩展的时间console.time();constvalue=searchValue.value;newPromise((resolve)=>{constexpanded=dataList.map((item:TreeDataItem)=>{if((item.labelasstring).indexOf(value)>-1){returngetParentKey(item.idasstring,options);}}returnnull;}.filter((item,i,self)=>item&&self.indexOf(item)===i);expandedKeys.value=expandedasstring[];autoExpandParent.value=true;resolve();}).then(async()=>{awaitnextTick();//记录模糊查询和dom扩展时的耗时console.timeEnd();});};页面效果测试耗时结果我们使用模糊查询on作为条件,先看有多少条数据满足on的条件,上图中满足模糊查询条件的数据有7831条,然后这些数据和它们的父节点必须相应地展开,我们再来看看耗时的示例代码21227.788818359375ms看起来很可怕试想一下哪个用户能在这里等20秒?解决方案首先,耗时长的原因文章开头也写了,下面说说根据这些原因优化的解决方案模糊查询的时间由于树数据是一个嵌套的数据结构,所以在查找目标节点的时候通常会进行深度遍历,深度遍历的时间自然会长一点,那么我们知道对于遍历来说,一个一维数组还是挺快的,那么可以先把树型数据转成平面数据,然后遍历找到目标节点,formatFlatTree方法就是接收树型结构数据,转成平面数据结构返回。那么这个就是很简单,下面的代码就不过多描述了,自己看吧lf/***@descriptionformattreedatastructureasflatdatastructure*@paramdata传入初始数据*@paramtreeDatareturn返回结果*@param_paramsreplaceinitialdata中的key,title,children字段是对应的树组件中的字段*@param_level层级*@paramparentIds父节点id集合用于设置pid*@param_params.其他自定义添加的需要返回的字段*/exportfunctionformatFlatTree(data,_params:any={},_level=1,parentIds:string[]=[],treeData:TreeDataState[]=[]){if(!data.length){returntreeData;}常量列表:TreeDataState[]=[];const参数={id:_params.id||'键',标签:_params.la贝尔||'标题',孩子:_params.children||'孩子',其他:_params.other||[],};constpIds:string[]=[];常量对象={};for(leti=0;i{obj[element]=node[element];});}treeData.push({id:key,label:node[param.label],pid:parentIds[i]||'0',level:_level,...obj,});list.push(...孩子);pIds.push(...newArray(child.length).fill(key));}returnformatFlatTree(list,param,_level+1,pIds,treeData);}优化查询到目标节点后展开相关节点的时间。然后我们查询到目标节点但是数据是扁平结构的,但是我们想要的是树结构,所以我们还需要将扁平数据合并为树结构数据,我们需要做的只是父节点和祖父母与目标节点相关的节点显示在页面上,不相关的给过滤掉不渲染,这样能节省渲染时间吗?获取目标节点及其相关节点,形成树导出函数getFilterTree(source,list){constinitData=JSON.parse(JSON.stringify(list));常量数据=JSON.parse(JSON.stringify(源));常量对象={};data.forEach((item)=>{obj[item.id]=item;});//合并完成整树data.forEach((item)=>{letpid=item.pid;while(pid){constparent=obj[pid];if(!parent){constorganParent=initData.find((item)=>item.id==pid);if(organParent){obj[pid]=organParent;pid=organParent.pid;data.push(organParent);}else{pid=null;}pid}else{;}}});consttrees=flatToTree(data,obj);return{data,trees};}exportfunctionflatToTree(data,obj){const树:TreeDataState[]=[];data.forEach((item)=>{constparent=obj[item.pid];if(parent){if(!item.children){item.children=[];}(parent.children||(parent.children=[])).push(item);}else{trees.push(item);}});returntrees;}来分析下面描述代码,看是做了什么事情getFilterTree方法接收两个参数source和listsource:表示符合查询条件的目标节点数据集list:表示整个扁平化的数据集通过JSON.parse(JSON.stringify())解决深拷贝问题,数据id作为key,以目标数据为value,通过forEach和whilepair查找符合条件的数据,将目标节点相关的父节点数据压入data中,相关数据以key的形式存储-valuepairsobj中的flatToTree方法接收两个参数data,objdata:与目标节点关联的所有数据obj:以键值对的形式存储与目标节点关联的所有数据继续通过forEach组合完整的树并返回树存储数据,返回树优化dom扩展执行批量操作任务。这个时候就得到了查询后的tree数据树,接下来需要优化dom扩展。根据getFilterTree方法,可以获取到数据。data是需要展开的dom节点。数据getTreeIds方法返回数据导出函数getTreeIds(list)中的id集合{constids:TreeDataState[]=[];list.map((item)=>{ids.push(item.id);});returnids;}下面的示例代码是将整个集合赋值给指定的树节点属性进行扩展。这种情况下会导致所有的dom节点一次性展开,所以expandedKeys.value=expanded肯定会导致页面卡顿。看我的解决方案,可以把扩展dom节点的任务拆分给它,也就是分批扩展扩展dom节点。这种方法效率很高,因为我们每次只扩展一些节点,不会操作一次。所有需要展开的节点functionspread(num:number,index=0,ids:string[]){constkeys:string[]=[];for(leti=0;i<50;i++){if(num<=0)中断;数--;索引++;keys.push(ids[索引]);}如果(努m>0){timer=setTimeout(()=>{returnspread(num,index,ids);},600);}expandedKeys.value=[...expandedKeys.value,...keys];}spread方法是接收三个参数num,index,ids进行本次任务拆分操作。num:表示ids的长度,需要展开多少个节点index:表示依次展开节点ids:表示需要展开的节点集合forloopi<50,表示每次展开50项,可以根据个人需要进行调整。if(num>0)表示还有未展开的节点,需要继续展开。setTimeout的意思是600ms扩展一次,可以根据个人需要调整。优化代码constflatTreeData=formatFlatTree(options,{id:'id',label:'label'});//Flattendataconsole.log(flatTreeData);constsearch=()=>{console.time();newPromise((resolve)=>{constval=searchValue.value;//获取目标节点数据constflatData=flatTreeData.filter((item)=>item.label.includes(val));//获取目标与它相关的节点数据,以及合并后的新树结构数据const{data,trees}=getFilterTree(flatData,flatTreeData);constids:string[]=getTreeIds(data);treeData.value=trees;//展开任务批量操作spread(ids.length,0,ids);resolve();}).then(async()=>{awaitnextTick();console.timeEnd();});};优化后,展示效果。我们来看看这时候用了多长时间。与示例代码21227.788818359375ms相比,耗时677.60302734375ms。数据优化效果还是很可观的。这时候页面上的数据少了很多,那么如何恢复原来的数据呢?最后,当搜索条件清空后,可以重新渲染原来的树数据,以3000条数据为基准,但很少用到这么大的数据量。然后继续改3000条测试数据,看耗时对比。继续查询,目标节点有1227个数据样本码。代码优化后,我们对比一下2166.179931640625ms和205.125244140625ms。然后我们可以看到结果是基于(3000)个节点,ant-design的平均渲染时间减少了(90%)。当然你也可以根据不同的节点数来测试一下会减少多少时间。结论扁平化和树结构数据转换方法可以看下面的文章。之前爆燃的高级前端开发不会把树转成平面格式。你真的会手写吗?