基于以前支持表单验证的el-table开发完成后,在数据量过大的时候,会出现渲染慢,表格卡顿等致命问题,而element-ui的el-table自己没有像antd同样提供虚拟列表的demo和相关支持,所以本文在上次的开发基础上,继而开展虚拟列表的开发。本次分为普通列表和树形列表两种,树形在普通列表上面多了一些状况考虑,例如展开收缩等。javascript
虚拟列表简单概述就是滚动分页,经过有限的视口来切片大量的数据,由于相比于js运算,渲染是一个很慢的过程,所以经过必定的js计算,保证更少的数据渲染,一般能够得到更好的用户体验。通常虚拟列表能够经过上下的动态padding值,来是滚动区域一直显示当前切片出的数据,以及经过transform的方法来动态移动可视区。transform这种方法理论上性能要好一些,由于浏览器渲染自己是分图层渲染的,而transform操做的视图,会被浏览器单独分层出来,渲染性能更优。
下图两种方式:
本次el-table 上的虚拟列表,采用了padding的方案,缘由是transform 会使el-table的样式混乱,若是是本身开发的table或者其余魔改支持度较好的插件的话优先transform。java
首先看一下el-table 渲染300 条的速度。
本次的测试代码有300条,自己并很少,可是有8列都是插槽中渲染的表单组件,所以渲染速度要慢不少,时间花销6s+。(antd 的table渲染要快一些,后面说缘由)node
增长虚拟列表后渲染速度:element-ui
计算总高度数组
height = list.length * 65 // height 为列表实际总高度 // 65 为每一行的行高,根据实际修改 // list为实际数据长度
计算上下padding值浏览器
paddingTop = scrollTop + "px"; paddingBottom = height - 10 * 65 - scrollTop + "px"; // scrollTop 为滚动的高度,即列表向下滚动的距离 // height 总高度 // 10为实际渲染的条数
监听列表滚动,动态为列表设置padding等样式。缓存
mounted() { console.time("render300条时间:"); this.form.rows = new Array(300).fill(0).map((v, i) => ({ name: i, children: [] })); this.form.rows = [...this.form.rows]; this.setIndex(this.form.rows); this.calcList(); this.$nextTick(() => { this.debounceFn = _.debounce(() => { this.scrollTop = this.$refs.table.bodyWrapper.scrollTop; }, 100); this.$refs.table.bodyWrapper.addEventListener("scroll", this.debounceFn); }); this.$nextTick(() => { console.timeEnd("render300条时间:"); }); },
监听的目标是这个: this.$refs.table.bodyWrapper,防抖的时间设置为100。antd
数组切片,渲染虚拟列表。数据结构
this.startIndex = Math.floor(scrollTop / 65); this.virtualRows = this.form.rows.slice( this.startIndex, this.startIndex + 10 );
根据滚动位置计算数组切片的起始点,而后截取相应的list渲染。app
上面说到el-table要比antd的table渲染更慢,其中一条缘由我我的认为是,el-table 在支持左右固定列的时候会克隆一份table,而后按照层级关系,使得UI上看到左右列的固定。若是左右都设置了fixed,就会有三个table同时在页面上。
而 Antd的table组件在左右fixed时就不会有这个问题,所以本人亲测在300条相同数据的情境下,Antd的性能要好很多。言归正传,要解决fixed的问题,就是要把这三个table的padding都去设置一遍才行,不然就会出现部分区域没有被顶下来而错位的状况。
let 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"; } });
找到当前table下的全部内容区域,遍历设置样式属性。
树形列表因为多一步展开折叠的操做,以及自己数据结构的缘由,数据预处理要复杂一下,不能直接slice,而要计算出相应区间而后生成新的数组。其次,在被收缩的子项是不渲染到table当中的,所以,要把被收缩的项排除在外。除了普通列表的几个step以外,树形列表还需有如下操做。
经过滚动计算出的起始点,以及可视区域的列表长度,能够获得一个区间,如【3,11】,即经过深度优先遍历(也是树形列表排列的顺序),找到第3到11条数据(不包含被折叠项),而后赋值到新的数组。
clacTree() { let count = 0; this.virtualRows = []; this.listLen = 0; const fn = arr => { for (let i = 0; i < arr.length; i++) { count++; this.listLen++; if (count >= 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) { let flag = false; node.children = []; const fn = arr => { arr.forEach(v => { if (node.pid === v.customIndex) { v.children.push(node); flag = true; } v.children && fn(v.children); }); }; fn(this.virtualRows); if (!flag) { this.virtualRows.push(node); } },
这里只对展开项进行操做,未展开的不去遍历和渲染,总高度也不计入。新数组赋值的时候,我经过二次遍历新的数组,再根据pid去push到相应位置,这种作法是由于实际业务须要,二次遍历中还有部分属性须要保持引用,以及部分属性是不可枚举的,深拷贝会丢失,若是只是截取树的一部分造成新的树,能够根据初始化获得的path属性,而后利用lodash的_.set 来完成。
el-table 的 expand-row-keys 传入一个数组,为默认的展开项,以后每次渲染都参考这个数组来决定列表是否展开,这个属性不能自动在展开收缩的时候把设置的 row-key 推入推出,而要手动的计算。在@expand-change事件中,来操做数组,以及判断被收缩的项有没有子集,若是有子集要给一个标记位,来为以后的列表渲染作准备。
expendRow(rows, expended) { // const this.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 (let i = 0; i < arr.length; i++) { fn(arr[i]); if (arr[i].children && arr[i].children.length > 0) { this.DFS_Array(arr[i].children, fn); } } }
在收缩以后因为把列表中的children整个移除,因此在el-table上面的展开箭头就不能正常显示了,由于在渲染数据中并无子节点,而实际数据中又是有子集的,因此,在上面增长的hasChild属性,就起到这个做用,他标记了数据被折叠,且有子集可展开的状况。所以,须要在列表中主动把展开的箭头加一下。
<el-table-column prop="customIndex" fixed label="序号" sortable width="180" v-slot="{$index, row}" > <span class="expanded-icon-box"> <i class="expanded-icon" v-if="row.hasChild" @click="expendRow(row,true)">></i> {{row.customIndex}} </span> </el-table-column>
至此 树形列表的虚拟列表也整合完毕了。本次示例代码不少地方比较仓促,待优化情景较多,除了拼凑新的树那里,还有滚动的缓存,若是树比较大的话,js的计算时间也要考虑入内,还有渲染的虚拟列表应该不从自己的第一位开始进入视口,这样的话,在必定范围的向上向下滚动,就能够必定程度的减小白屏。
虚拟列表经过减小实际渲染数据来优化性能,在不对element-ui作较大改动的状况下,知足了大量数据,包括树形的结构数据的渲染场景。若是考虑以前的列表的表单验证的情景,须要让部分属性脱离引用,如children,不然会污染源数据,其次让表单数据保持引用关联,这样就没必要专门给表单组件设置事件,来匹配源数据的改动,即直接将新的列表的item的表单对象等于老的相应表单对象便可。