一 需求背景
需求:在一个表格里面一次性渲染全部数据,不采用分页形式,每行数据都有Echart图插入。
问题:图表渲染卡顿
技术栈:Vue、Element UI
卡顿原因:页面渲染时大量的元素参与到了重排的动作中,性能差
优化方案:虚拟滚动
二 虚拟滚动原理
虚拟滚动
其实就是综合数据分页和无限滚动的方法,在有限的视口中只渲染我们所能看到的数据,超出视口之外的数据就不进行渲染
,可以通过计算可视范围内的单元格,保证每一次滚动渲染的DOM元素都是可以控制的,不会担心像数据分页一样一次性渲染过多,也不会发生像无限滚动方案那样会存在数据堆积,是一种很好的解决办法。
假设实际开发中服务端一次响应20万条列表数据,此时设备屏幕只允许容纳20条,那么用户理论上只可以看见20条数据,其他的数据不会进行渲染加载。如果前端将20万条数据全部渲染成DOM元素,可能造成程序卡顿,占用较大资源,非常影响用户体验,那么虚拟滚动技术就完美的解决了这一问题。
可以计算:卷入行数 = scrollTop(卷入高度) / 每行的高度(itemH)
如何计算可视区域渲染的元素以及实现虚拟滚动,步骤如下:
- 统一设置每一行的
高度
需要相同
,方便计算。 - 需要计算渲染数据数量(数组的长度),根据每行的高度以及元素的总量计算整个DOM渲染容器的高度。
- 获取可视区域的高度
- 触发滚动事件后,计算偏移量(滚动条据顶距离),再根据可视区域高度计算本次偏移的截止量,得到需要渲染的具体数据。
- 对于与表格的列来说,需要做虚拟滚动的话,在x轴同样可以根据以上步骤执行,实现横向虚拟滚动。
三 项目具体代码:
<el-table
ref="latestPositionRef"
v-loading="tableLoading"
class="table-fixed"
size="mini"
:data="sliceTable"
height="355px"
:cell-style="cellStyle"
row-key="secId"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
<el-table-column label="排名" width="50px" align="center" fixed>
<template slot-scope="scope">{{ scope.$index + 1 }}</template>
</el-table-column>
<DynamicColumn
v-for="(ite, index) in overviewColumns"
:key="index"
:item="ite"
:empty="1"
:data-list="infoList"
table-sign="latest-position-list"
:schemas="overviewColumns"
@changeColumn="(cols)=>{overviewColumns=cols;}"
/>
<el-table-column label="备注设置" align="center" width="50" fixed="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.secId"
type="text"
size="mini"
plain
@click="setRemark(scope.row)"
>查看</el-button>
</template>
</el-table-column>
</el-table>
data() {
return {
// 动态列-ETF精选
overviewColumns: this.$columns.getColumns('etf_selected_list'),
// 表格数据
infoList: [],
showInfoList: [],
// 开始索引
startIndex: 0,
// 空元素,用于撑开table的高度
vEle: undefined,
// 每一行高度
itemHeight: 42,
}
}
watch: {
sliceTable: {
handler() {
// 解决表格错位问题
this.$nextTick(() => {
this.$refs.latestPositionRef.doLayout()
})
},
deep: true
},
}
async created() {
// 创建一个空元素,这个空元素用来撑开 table 的高度,模拟所有数据的高度
this.vEle = document.createElement('div')
},
computed: {
// 这个是截取表格中的部分数据,放到了 table 组件中来显示
sliceTable() {
return this.showInfoList.slice(this.startIndex, this.startIndex + 6)
},
}
/** 加载ETF精选列表数据 */
loadData() {
console.log('loadData')
const start_i = this.showInfoList.length
for (let i = start_i; i < start_i + 10; i++) {
this.showInfoList.push(this.infoList[i])
}
this.$nextTick(() => {
// 设置成绝对定位,这个元素需要我们去控制滚动
this.$refs.latestPositionRef.$el.querySelector(
'.el-table__body'
).style.position = 'absolute'
// 计算表格所有数据所占内容的高度
this.vEle.style.height =
this.showInfoList.length * this.itemHeight + 'px'
// 把这个节点加到表格中去,用它来撑开表格的高度
this.$refs.latestPositionRef.$el
.querySelector('.el-table__body-wrapper')
.appendChild(this.vEle)
// 重新设置曾经被选中的数据
this.selection.forEach((row) => {
this.$refs.latestPositionRef.toggleRowSelection(row, true)
})
})
},
tableScroll() {
console.log('tableScroll')
const bodyWrapperEle = this.$refs.latestPositionRef.$el.querySelector(
'.el-table__body-wrapper'
)
console.log(bodyWrapperEle, 'bodyWrapperEle')
// 滚动的高度
const scrollTop = bodyWrapperEle.scrollTop
// 下一次开始的索引
this.startIndex = Math.floor(scrollTop / this.itemHeight)
// 滚动操作
bodyWrapperEle.querySelector(
'.el-table__body'
).style.transform = `translateY(${this.startIndex * this.itemHeight}px)`
// 滚动操作后,上面的一些 tr 没有了,所以需要重新设置曾经被选中的数据
this.selection.forEach((row) => {
this.$refs.latestPositionRef.toggleRowSelection(row, true)
})
// 滚动到底,加载新数据
if (
bodyWrapperEle.scrollHeight <=
scrollTop + bodyWrapperEle.clientHeight
) {
if (this.showInfoList.length === this.infoList.length) {
this.$message.warning('没有更多了')
return
}
this.loadData()
// 解决el-table中内容错位
// this.$nextTick(() => {
// this.$refs.latestPositionRef.doLayout()
// })
}
}
四 其他方案:
分段渲染、数据分页、无限滚动