element表格el-table组件实现虚拟滚动,解决数据量大渲染DOM过多而卡顿问题
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 // 延迟
}
},
watch: {
num: function(newV) {
// 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
if (newV > 1) {
this.start = (newV - 1) * this.pageList
this.end = (newV + 2) * this.pageList
// 使用延时器会造成页面短暂白屏的问题,使用requestAnimationFrame则可以解决
// 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)
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)`
}
})
// 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)
}
}
},
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))
}
}
}
</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>
使用虚拟列表做投屏功能
投屏页面 buletinFullscreen.vue
<!--
* @Last Modified by: Damon
* @Last Modified time: 2023-2-14
* @content: 订单看板投屏
-->
<template>
<!-- 设置虚拟列表滚动条的盒子 -->
<div id="buletinFullscreen">
<div class="list-top">
<p class="list-tit">订单看板</p>
<ul class="list-lists">
<li>已接单(数量): <span style="color: #008300">1</span></li>
<li>生产中(数量): <span style="color: #D3831A">2</span></li>
<li>出货中(数量): <span style="color: #128BFF">3</span></li>
<li>出货完成(数量): <span style="color: #585858">4</span></li>
</ul>
<button
class="cancel-btn"
size="mini"
@click="exitListFullScreen"
>{{ $t('qxtp') }}</button>
</div>
<!-- 设置虚拟列表这个盒子的总高度,即所有数据显示所需要的高度,比如100*163,其中100为所有数据条数,163为每条列表展示的高度 -->
<div class="list-div-content" id="buletinScrollBox" ref="contentBox">
<!-- 真正展示内容的盒子,也是设置transform的盒子 -->
<div id="buletinScrollContent" v-if="tableDataFullscreen && tableDataFullscreen.length>0">
<!-- 每一项 -->
<div class="list-title-div" v-for="(item) in tableDataFullscreen" :key="item.id">
{{ item.code }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {},
data () {
return {
tableDataFullscreen: [], // 需要显示的数据
scrollDomBox: null, // 设置了滚动的那个盒子
noScroll: true, // 是否没有滚动条
isBottom: false,
setIntervalTimer: null,
setTimeoutTimer: null,
setTimeoutTimer1: null,
allTableDataFullscreen: [], // 所有数据
tableWarp: null,
scrollTop: 0,
num: 0,
start: 0,
end: 90, // 3倍的pageList
starts: 0, // 备份[保持与上一样]
ends: 90, // 备份[保持与上一样]
pageList: 30, // 需要显示的数据的条数
clientList: 10, // 可视区域能显示的条数
itemHeight: 110, // 每一行高度
}
},
watch: {
isBottom: function() {
// 触底和触顶后重新请求一次数据
setTimeout(() => {
this.clearIntervalFn()
this.getListData(true)
},5000)
},
num: function(newV) {
// 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
console.log(newV,'newV');
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)`
this.tableDataFullscreen = this.allTableDataFullscreen.slice(this.start, this.end)
})
} else {
requestAnimationFrame(() => {
this.tableDataFullscreen = this.allTableDataFullscreen.slice(this.starts, this.ends)
this.tableWarp.style.transform = `translateY(0px)`
})
}
}
},
created() {},
mounted() {
this.getListData()
},
destroyed() {
// 清除定时器和延时器
this.clearIntervalFn()
this.clearTimeoutFn(this.setTimeoutTimer)
this.clearTimeoutFn(this.setTimeoutTimer1)
// 取消监听全屏事件
this.removeScreenFn()
},
methods: {
// 计算当前屏数
onScroll() {
this.scrollTop = this.scrollDomBox.scrollTop
this.num = Math.floor(this.scrollTop / (this.itemHeight * this.pageList))
},
clearIntervalFn() {
clearInterval(this.setIntervalTimer)
},
clearTimeoutFn(timer) {
clearTimeout(timer)
},
getListData(isFalg) {
if (!isFalg) {
this.listenerScreenFn()
}
let arr = []
// 模拟异步请求
setTimeout(() => {
for (var i = 0; i < 10000; i++) {
let obj = {
code: '列表' + i
}
arr.push(obj)
}
this.allTableDataFullscreen = arr;
// 首屏的时候要多加上一屏的数据才能无缝衔接
const count = this.pageList + this.clientList;
this.tableDataFullscreen = this.allTableDataFullscreen.slice(0,count)
this.$nextTick(() => {
// 设置了滚动的盒子
this.scrollDomBox = document.getElementById("buletinFullscreen");
// 高度设置为所有数据所需要的总高度
let divWarpPar = document.getElementById('buletinScrollBox')
// 如果这里还没获取到数据就渲染会导致内容盒子高度为0,可以通过监听长度后再设置一次高度
// 头部高度为80
if (divWarpPar) {
divWarpPar.style.height = this.allTableDataFullscreen.length * this.itemHeight + 80 + 'px'
}
console.log(this.allTableDataFullscreen.length * this.itemHeight,'kkk');
// 被设置的transform元素
this.tableWarp = document.getElementById('buletinScrollContent')
this.scrollDomBox && this.scrollDomBox.addEventListener('scroll', this.onScroll)
let scrollBox = document.getElementById("buletinFullscreen");
// 电脑屏幕的刷新率为60左右
this.setIntervalTimer = setInterval(() => {
this.autoScroll(scrollBox)
this.onScroll()
},60)
})
},2000)
},
// 自动滚动
autoScroll(scrollDom) {
if ((scrollDom.scrollTop + scrollDom.clientHeight < scrollDom.scrollHeight && !this.isBottom) || !scrollDom.scrollTop) {
scrollDom.scrollTop += 1
this.isBottom = false
} else {
// 触底则向上滚动
scrollDom.scrollTop -= 1
this.isBottom = true
}
if (!scrollDom.scrollTop && this.noScroll) {
this.noScroll = false
// 没有滚动条时触发
this.setTimeoutTimer1 = setTimeout(() => {
this.clearIntervalFn()
this.getListData()
},30000)
}
},
/**
* @description 全屏
* requestFullscreen方法必须由用户主动交互触发,否则会报错
*/
listFullScreen() {
const element = document.documentElement
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
}
},
/**
* @description 退出全屏
*/
exitListFullScreen() {
if (document.fullscreenElement) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
}
},
changeFullscreen() {
console.log('监听全屏事件');
const fullscreenEle = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement
console.log(fullscreenEle,'fullscreenEle');
let flag = false
if (fullscreenEle) {
//浏览器进入全屏
flag = true
} else {
flag = false
}
this.$emit('changeShowFlag',flag)
},
/**
* @description 监听全屏事件
*/
listenerScreenFn() {
this.listFullScreen()
if (document.exitFullscreen) {
document.addEventListener('fullscreenchange', this.changeFullscreen)
} else if (document.mozCancelFullScreen) {
document.addEventListener('mozfullscreenchange', this.changeFullscreen)
} else if (document.msExitFullscreen) {
document.addEventListener('MSFullscreenChange', this.changeFullscreen)
} else if (document.webkitCancelFullScreen) {
document.addEventListener('webkitfullscreenchange', this.changeFullscreen)
}
},
/**
* @description 移除全屏事件
*/
removeScreenFn() {
if (document.exitFullscreen) {
document.removeEventListener('fullscreenchange', this.changeFullscreen)
} else if (document.mozCancelFullScreen) {
document.removeEventListener('mozfullscreenchange', this.changeFullscreen)
} else if (document.msExitFullscreen) {
document.removeEventListener('MSFullscreenChange', this.changeFullscreen)
} else if (document.webkitCancelFullScreen) {
document.removeEventListener('webkitfullscreenchange', this.changeFullscreen)
}
},
}
}
</script>
<style lang="less" scoped>
#buletinFullscreen {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow-y: auto;
z-index: 1002;
.list-top{
position: fixed;
z-index: 1003;
width:100%;
transition: top .3s ease;
border-bottom:1px solid #ebeef2 ;
margin-left: 0px;
padding: 0 15px;
height: 80px;
display: flex;
align-items: center;
color: #333333;
.cancel-btn {
position: absolute;
top: 25px;
right: 15px;
}
}
.list-tit{
margin-right: 30px;
margin-left: 10px;
font-weight: 500;
font-size: 24px;
}
.list-lists{
display: flex;
margin-top: 0px;
li{
margin-right: 30px;
}
}
.list-div-content{
padding: 95px 15px 15px;
background-color: #ebeef2;
min-height: 100%;
overflow: hidden;
.list-title-div{
height: 100px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px 15px;
background: #ffffff;
margin-bottom: 10px;
position: relative;
overflow: hidden;
}
}
}
</style>
引入投屏页
<template>
<div>
<button
size="mini"
@click="handlerClick"
>投屏</button>
<!-- 投屏页面 必须使用v-if -->
<buletin-fullscreen v-if="isShowFullScreen" @changeShowFlag="handlerClick"></buletin-fullscreen>
</div>
</template>
<script>
import buletinFullscreen from '~/components/bulletin/buletinFullscreen.vue';
export default {
components: {
buletinFullscreen
},
data() {
return {
isShowFullScreen:false,// 是否全屏
};
},
mounted() {
window.addEventListener("keydown", this.keyDownFuns)// 监听按键事件
},
methods: {
// 监听F11键盘事件
keyDownFuns(event) {
if (event.keyCode === 122) {
event.preventDefault();
this.handlerClick(true)
}
},
handlerClick(flag) {
this.isShowFullScreen= flag
}
}
};
</script>
<style lang="less" scoped>
</style>
更多推荐
所有评论(0)