赞
踩
当页面数据过多,前端渲染大量的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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。