当前位置: 首页 > Web前端 > vue.js

基于element-uiel-table开发虚拟列表(树列表)

时间:2023-03-31 23:45:47 vue.js

前言基于之前开发的支持表单验证的el-table,当数据量过大时,渲染会很慢,表单会freeze等致命问题,而且element-ui的el-table本身并没有像antd一样提供虚拟列表的demo和相关支持,所以本文会在之前开发的基础上继续开发虚拟列表.这次分为普通链表和树链表两种。树形有一些对普通链表的考虑,比如扩展和收缩。虚拟列表的简单概述就是滚动和分页,通过有限的视口切片大量的数据,因为渲染相对于js操作是一个非常慢的过程,所以通过一定的js计算,保证较少的数据渲染,往往对于一个更好的用户体验。一般虚拟列表可以通过上下动态padding值在滚动区域显示当前切片的数据,通过transform方法动态移动可见区域。transform方式理论上性能更好,因为浏览器渲染本身是分层渲染的,transform操作的view会被浏览器单独分层,渲染性能更好。下图有两种方式:el-table上的virtuallist这次采用padding方案,因为transform会混淆el-table的样式,如果是自研表或者其他插件有了更好的魔法修改支持,则优先考虑转换。普通列表先看el-table渲染300条的速度。这次有300个测试代码,不算多,但是8列都是slot渲染的表单组件,所以渲染速度慢很多,需要6s+。(antd的表格渲染速度更快,后面会解释原因)提高虚拟列表的渲染速度后:开发过程step1计算总高度height=list.length*65//height为列表的实际总高度//65为每行的行高,根据实际修改//list为实际数据长度计算上下padding值step2paddingTop=scrollTop+"px";paddingBottom=height-10*65-scrollTop+"px";//scrollTop为滚动高度,即列表向下滚动的距离//height总高度//10为实际渲染的条数。step3监听列表滚动,动态设置列表的padding等样式。mounted(){console.time("render300时间:");this.form.rows=newArray(300).fill(0).map((v,i)=>({name:i,children:[]}));this.form.rows=[...this.form.rows];this.setIndex(this.form.rows);这个.calcList();this.$nextTick(()=>{this.debounceFn=_.debounce(()=>{this.scrollTop=this.$refs.table.bodyWrapper.scrollTop;},100);this.$refs.table。bodyWrapper.addEventListener("滚动",this.debounceFn);});this.$nextTick(()=>{console.timeEnd("render300time:");});},监控的目标是这样的:this.$refs.table.bodyWrapper,防抖时间设置为100。step4对数组进行切片,渲染出一个虚拟列表。this.startIndex=Math.floor(scrollTop/65);this.virtualRows=this.form.rows.slice(this.startIndex,this.startIndex+10);根据滚动位置计算出数组切片的起始点,然后截取相应的列表渲染。Supportcolumnfixed(Table-columnAttributes-fixed)上面说了el-table比antd的table渲染慢。我个人认为的原因之一是el-table在支持左右固定列时会克隆一个表。然后根据层级关系,将左右两列固定在UI上。如果左右都设置fixed,那么页面上会同时出现三个表格。Antd的table组件在left和right固定的情况下不会出现这个问题,所以我个人测试了300条相同数据的情况下Antd的性能要好很多。言归正传,解决fixed问题,有必要对这三个表的padding进行一次设置,否则会出现部分区域没有下推错位的情况。让mainTable=this.$refs.table.$el.getElementsByClassName("el-table__body");Array.from(mainTable).forEach(v=>{v.style.height=height+"px";if(this.startIndex+10>=this.num){//因为el-table会晃动滚动到最后,所以增加判断,单独设置属性v.style.paddingTop=scrollTop-65+"px";v.style.paddingBottom=0;}else{v.style.paddingTop=scrollTop+"px";v.style.paddingBottom=height-10*65-scrollTop+"px";}});查找当前表Region下的所有内容,遍历设置样式属性。树列表由于树列表的一步展开和折叠操作,以及其自身的数据结构,数据预处理比较复杂。不能直接切片,而是要计算出对应的区间,然后生成新的数组。其次,收缩后的子项并没有渲染到表中,所以应该排除收缩后的项。除了普通列表的几个步骤外,树列表还需要以下操作。通过滚动数组切片计算出的起始点和可见区域的链表长度可以得到一个区间,比如[3,11],即通过深度优先遍历(也是树链表的顺序),找到第3到第11个数据(不包括折叠项),然后分配给一个新数组。clacTree(){让计数=0;this.virtualRows=[];这个.listLen=0;constfn=arr=>{for(leti=0;i=this.startIndex&&count<=this.startIndex+10){this.combineArr(_.cloneDeep(arr[i]));}arr[i].children&&arr[i].expended==="true"&&fn(arr[i].children);}};fn(this.form.rows);},combineArr(node){letflag=false;node.children=[];constfn=arr=>{arr.forEach(v=>{if(node.pid===v.customIndex){v.children.push(node);flag=true;}v.children&&fn(v.孩子们);});};fn(this.virtualRows);if(!flag){this.virtualRows.push(node);}},这里只对展开的项进行操作,未展开的项不进行遍历和渲染,总高度也不计算。我在分配新数组的时候,遍历两次新数组,然后根据pidpush到对应的位置。这是因为实际业务需要。第二次遍历,还有一些属性需要引用,还有一些属性是没有的。对于枚举,深拷贝会丢失。如果只是截取树的一部分组成一棵新树,可以根据初始化得到的path属性,使用lodash的_.set来完成。el-table的expand-row-keys的扩容和缩容传入一个数组,就是默认的扩容项。之后每次渲染都会引用这个数组来决定是否展开列表。该属性不能在展开和收缩时自动设置行键。钥匙推入推出,但需要人工计算。@expand-change事件中,操作数组,判断是否存在约定项的子集。如果有子集,则需要一个flag,为后续的列表渲染做准备。expendRow(rows,expended){//constthis.DFS_Array(this.form.rows,v=>{if(v.customIndex==rows.customIndex){v.expended=String(expended);v.hasChild=v.expended==="false"&&v.children.length>0?true:false;}});if(!expended){this.expendArrs=this.expendArrs.filter(v=>v!==rows.customIndex);}else{this.expendArrs.push(rows.customIndex);}this.calcList(this.scrollTop);},DFS_Array(arr,fn){for(leti=0;i0){this.DFS_Array(arr[i].children,fn);}}}在shrink之后,因为列表中的所有子节点都被移除了,导致el-table上的展开箭头不能正常显示,因为渲染数据中没有子节点,而实际数据中是有子节点的,所以,在上面添加的hasChild属性就起到了这个作用。它标志着数据被折叠并且可以扩展子集的情况。因此,有必要主动将展开的箭头添加到列表中。>{{row.customIndex}}至此,树状列表的虚拟列表也整合完毕。示例代码很多地方比较仓促,需要优化的场景也很多。除了拼凑一棵新树,还有一个滚动缓存。如果树比较大,还要考虑js的计算时间,渲染出来的虚拟列表本身不要从一开始就进入viewport,这样可以一定程度上减少白屏在一定范围内上下滚动。总结虚拟列表通过减少实际渲染数据来优化性能,在不对element-ui做大改动的情况下满足大量数据的渲染场景,包括树状结构数据。如果考虑前面列表的表单验证场景,需要去掉引用中的一些属性,比如children,否则会污染源数据,其次,让表单数据保持引用关联,这样就不用必须为表单组件设置事件来匹配源数据的变化,即直接将新列表的item的表单对象等同于对应的旧表单对象。