一、此功能已集成到TTable组件中
二、最终效果
三、需求
某些页面不做分页时,当数据过多,会导致页面卡顿,甚至卡死
四、虚拟滚动
一、固定一个
可视区域
的大小并且其大小是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值也就是可视范围内需要展示的内容,而可视区域之外的元素均可以不做渲染。
二、如何计算可视区域内需要渲染的元素,我们通过如下几步来实现虚拟滚动:1、每一行的高度需要相同,方便计算。
2、需要知道渲染的数据量(数组长度),可基于总量和每个元素的高度计算出容器整体的所需高度,这样就可以伪造一个真实的滚动条。
3、获取可视区域的高度。
4、在滚动事件触发后,滚动条的距顶距离即这个数据量中的偏移量,再根据可视区域本身的高度,算出本次偏移量,这样就得到了需要渲染的具体数据
五、具体实现(源码)
<template>
<div class="t-table" id="t_table">
<el-table
ref="el-table"
:data="tableData"
:class="{
cursor: isCopy,
row_sort: isRowSort,
highlightCurrentRow: highlightCurrentRow,
radioStyle: table.firstColumn && table.firstColumn.type === 'radio',
treeProps: isShowTreeStyle,
is_sort_icon:onlyIconSort
}"
:max-height="useVirtual?maxHeight||540:maxHeight"
v-bind="$attrs"
v-on="$listeners"
:highlight-current-row="highlightCurrentRow"
:border="table.border || isTableBorder"
:span-method="spanMethod || objectSpanMethod"
:cell-class-name="cellClassNameFuc"
@sort-change="soltHandle"
@row-click="rowClick"
@cell-dblclick="cellDblclick"
>
<!-- 主体内容 -->
<template v-for="(item, index) in renderColumns">
<el-table-column
v-if="item.isShowCol === false ? item.isShowCol : true"
:key="index + 'i'"
:type="item.type"
:label="item.label"
:prop="item.prop"
:min-width="item['min-width'] || item.minWidth || item.width"
:sortable="item.sort || sortable"
:align="item.align || 'center'"
:fixed="item.fixed"
:show-overflow-tooltip="useVirtual?true:item.noShowTip?false:true"
v-bind="{ ...item.bind, ...$attrs }"
v-on="$listeners"
>
<template slot-scope="scope">
...
</template>
</el-table-column>
</template>
</el-table>
</div>
</template>
<script>
export default {
name: 'TTable',
props: {
// table所需数据
table: {
type: Object,
default: () => {
return {}
}
// required: true
},
// 表头数据
columns: {
type: Array,
default: () => {
return []
}
// required: true
},
...
// Table最大高度
maxHeight: {
type: [String, Number]
},
// 是否开启虚拟列表
useVirtual: {
type: Boolean,
default: false
}
},
data() {
return {
tableData: this.table?.data,
/**
* 虚拟列表
*/
saveDATA: [], // 所有数据
tableRef: null, // 设置了滚动的那个盒子
tableWarp: null, // 被设置的transform元素
fixLeft: null, // 固定左侧--设置的transform元素
fixRight: null, // 固定右侧--设置的transform元素
tableFixedLeft: null, // 左侧固定列所在的盒子
tableFixedRight: null, // 右侧固定列所在的盒子
scrollTop: 0,
scrollNum: 0, // scrollTop / (itemHeight * pageList)
start: 0,
end: 30, // 3倍的pageList
starts: 0, // 备份
ends: 30, // 备份
pageList: 10, // 一屏显示
itemHeight: 48 // 每一行高度
}
},
watch: {
'table.data': {
handler(val) {
if (this.useVirtual) {
this.saveDATA = val
this.tableData = this.saveDATA.slice(this.start, this.end)
} else {
this.tableData = val
}
},
deep: true // 深度监听
},
scrollNum(newV) {
// 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
if (newV > 1) {
this.start = (newV - 1) * this.pageList
this.end = (newV + 2) * this.pageList
requestAnimationFrame(() => {
// 计算偏移量
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)
})
} else {
requestAnimationFrame(() => {
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)`
}
})
}
}
},
created() {
// 是否开启虚拟列表
if (this.useVirtual) {
this.init()
}
},
mounted() {
// 是否开启虚拟列表
if (this.useVirtual) {
this.initMounted()
}
},
methods: {
initMounted() {
this.$nextTick(() => {
// 设置了滚动的盒子
this.tableRef = this.$refs['el-table'].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)
})
},
// 初始化数据
init() {
this.saveDATA = this.table?.data
this.tableData = this.saveDATA.slice(this.start, this.end)
},
// 滚动事件
onScroll() {
this.scrollTop = this.tableRef.scrollTop
this.scrollNum = Math.floor(this.scrollTop / (this.itemHeight * this.pageList))
}
}
}
</script>
六、源码地址
GitHub源码地址
Gitee源码地址
基于ElementUi或Antd再次封装基础组件文档
vue3+ts基于Element-plus再次封装基础组件文档