element表格el-table组件实现虚拟滚动,解决数据量大渲染DOM过多而卡顿问题
当页面数据过多,前端渲染大量的DOM时,会造成页面卡死问题,使用分页或则懒加载这些方案也无法解决,这些处理方法在页面加载到足够多的数据的时候,随着页面追加渲染的DOM越来越多,也会导致页面卡顿,甚至卡死。
这时候我们可以把两个方案中和一下,既然在有限的视窗中我们只能看到一部分的数据,那么我们就通过计算可视范围内的单元格,这样就保证了每一次拖动,我们渲染的 DOM 元素始终是可控的,不会像数据分页方案怕一次性渲染过多,也不会发生无限滚动方案中的老数据堆积现象。所以就有了虚拟滚动这一方案。
虚拟滚动
接下来我们用一张图来表示虚拟滚动的表现形式。
根据图中我们可以看到,无论我们如何滚动,我们可视区域的大小其实是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值也就是可视范围内需要展示的内容,也就是图中的绿色区块,在可视区域之外的元素均可以不做渲染。
那么问题就来了,如何计算可视区域内需要渲染的元素,我们通过如下几步来实现虚拟滚动:
每一行的高度需要相同,方便计算
需要得知渲染的数据量(数组长度),可基于总量和每个元素的高度计算出容器整体的所需高度,这样就可以伪造一个真实的滚动条
获取可视区域的高度
在滚动事件触发后,滚动条的距顶距离也可以理解为这个数据量中的偏移量,再根据可视区域本身的高度,算出本次偏移的截止量,这样就得到了需要渲染的具体数据
如果类似于渲染一个宽表,单行可横向拆分为多列,那么在X轴上同理实现一次,就可以横向的虚拟滚动
效果如图:
<template> <el-table :data="tableData" ref="tableRef" style="width: 900px" max-height="380" border stripe class="myTable" > <el-table-column prop="date" label="必要元素:" min-width="150" align="center" fixed="left" > </el-table-column> <el-table-column label="每一行高度必须相同"> <el-table-column prop="name" label="class不能为【myTable】" min-width="180" align="center" > </el-table-column> <el-table-column label="ref不能为【tableRef】"> <el-table-column prop="province" label="省份" min-width="150" align="center" > </el-table-column> <el-table-column prop="city" label="市区" min-width="150" align="center" > </el-table-column> <el-table-column prop="address" label="地址" min-width="150" align="center" > </el-table-column> </el-table-column> </el-table-column> <el-table-column label="操作" fixed="right" min-width="160" align="center"> <template> <el-button size="mini">编辑</el-button> <el-button size="mini" type="danger">删除</el-button> </template> </el-table-column> </el-table> </template> <script> export default { data() { return { tableData: [], // 需要渲染的数据 saveDATA: [], // 所有数据 tableRef: null, // 设置了滚动的那个盒子 tableWarp: null, fixLeft: null, fixRight: null, tableFixedLeft: null, tableFixedRight: null, scrollTop: 0, num: 0, start: 0, end: 42, // 3倍的pageList starts: 0, // 备份[保持与上一样] ends: 42, // 备份[保持与上一样] pageList: 14, // 一屏显示 itemHeight: 41, // 每一行高度 timeOut: 400 // 延迟 } }, created() { this.init() }, mounted() { this.$nextTick(() => { // 设置了滚动的盒子 this.tableRef = this.$refs.tableRef.bodyWrapper // 左侧固定列所在的盒子 this.tableFixedLeft = document.querySelector( '.el-table .el-table__fixed .el-table__fixed-body-wrapper' ) // 右侧固定列所在的盒子 this.tableFixedRight = document.querySelector( '.el-table .el-table__fixed-right .el-table__fixed-body-wrapper' ) /** * fixed-left | 主体 | fixed-right */ // 主体改造 // 创建内容盒子divWarpPar并且高度设置为所有数据所需要的总高度 let divWarpPar = document.createElement('div') // 如果这里还没获取到saveDATA数据就渲染会导致内容盒子高度为0,可以通过监听saveDATA的长度后再设置一次高度 divWarpPar.style.height = this.saveDATA.length * this.itemHeight + 'px' // 新创建的盒子divWarpChild let divWarpChild = document.createElement('div') divWarpChild.className = 'fix-warp' // 把tableRef的第一个子元素移动到新创建的盒子divWarpChild中 divWarpChild.append(this.tableRef.children[0]) // 把divWarpChild添加到divWarpPar中,最把divWarpPar添加到tableRef中 divWarpPar.append(divWarpChild) this.tableRef.append(divWarpPar) // left改造 let divLeftPar = document.createElement('div') divLeftPar.style.height = this.saveDATA.length * this.itemHeight + 'px' let divLeftChild = document.createElement('div') divLeftChild.className = 'fix-left' this.tableFixedLeft && divLeftChild.append(this.tableFixedLeft.children[0]) divLeftPar.append(divLeftChild) this.tableFixedLeft && this.tableFixedLeft.append(divLeftPar) // right改造 let divRightPar = document.createElement('div') divRightPar.style.height = this.saveDATA.length * this.itemHeight + 'px' let divRightChild = document.createElement('div') divRightChild.className = 'fix-right' this.tableFixedRight && divRightChild.append(this.tableFixedRight.children[0]) divRightPar.append(divRightChild) this.tableFixedRight && this.tableFixedRight.append(divRightPar) // 被设置的transform元素 this.tableWarp = document.querySelector( '.el-table .el-table__body-wrapper .fix-warp' ) this.fixLeft = document.querySelector( '.el-table .el-table__fixed .el-table__fixed-body-wrapper .fix-left' ) this.fixRight = document.querySelector( '.el-table .el-table__fixed-right .el-table__fixed-body-wrapper .fix-right' ) this.tableRef.addEventListener('scroll', this.onScroll) }) }, methods: { init() { this.saveDATA = [] for (let i = 0; i < 10000; i++) { this.saveDATA.push({ date: i, name: '王小虎' + i, address: '1518', province: 'github:', city: 'divcssjs', zip: 'divcssjs' + i }) } this.tableData = this.saveDATA.slice(this.start, this.end) }, onScroll() { this.scrollTop = this.tableRef.scrollTop this.num = Math.floor(this.scrollTop / (this.itemHeight * this.pageList)) } }, watch: { num: function(newV) { // 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量 if (newV > 1) { this.start = (newV - 1) * this.pageList this.end = (newV + 2) * this.pageList setTimeout(() => { // 计算偏移量 this.tableWarp.style.transform = `translateY(${this.start * this.itemHeight}px)` if (this.fixLeft) { this.fixLeft.style.transform = `translateY(${this.start * this.itemHeight}px)` } if (this.fixRight) { this.fixRight.style.transform = `translateY(${this.start * this.itemHeight}px)` } this.tableData = this.saveDATA.slice(this.start, this.end) }, this.timeOut) } else { setTimeout(() => { this.tableData = this.saveDATA.slice(this.starts, this.ends) this.tableWarp.style.transform = `translateY(0px)` if (this.fixLeft) { this.fixLeft.style.transform = `translateY(0px)` } if (this.fixRight) { this.fixRight.style.transform = `translateY(0px)` } }, this.timeOut) } } } } </script> <style lang="less" scoped> .myTable { /deep/ td { padding: 6px 0 !important; } } /*滚动条样式*/ /deep/ .el-table__body-wrapper::-webkit-scrollbar { /*滚动条整体样式*/ width: 6px; /*高宽分别对应横竖滚动条的尺寸*/ height: 8px; } /deep/ .el-table__body-wrapper::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius: 2px; background: #666; } /deep/ .el-table__body-wrapper::-webkit-scrollbar-track { /*滚动条里面轨道*/ background: #ccc; } </style>