参考链接如下:
https://blog.csdn.net/ZYS10000/article/details/122294309
https://blog.csdn.net/qq_44993023/article/details/125680903
1、使用场景/背景:
表格设置分页或者不设置分页,但是当前页数会显示到几百至上千条时,会造成 dom 渲染的负担,页面以及鼠标卡顿,导致用户体验不好,此种情况,就可以使用虚拟列表渲染来解决一下子渲染 [行数 * 列数]个控件的渲染负担问题。
2、实现思路:
一、虚拟列表是什么?
虚拟列表就是只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而减轻 dom 操作的负担。
![](https://img.haomeiwen.com/i17273442/2efb41c29fcccf41.png)
二、实现思路
1)用一个 container 来撑起所有行的高度,为总的不可见区域(模拟实际滚动区域),利用其对 scroll
事件的监听,通过 scrollTop
来计算渲染数组的起始索引startIndex
。
2)用一个 container 来撑起当前渲染行的数据,为当前可视区域,该高度应该提前设定或者为固定值(最大高度应该是屏幕的最大可视区域),通过 【该高度 / 行的高度】 从而确定能够显示的行数showNum
。
3)通过对 startIndex
和 showNum
的配置可以对需要渲染的数据进行选择性渲染。
3、代码实现:
// 代码可能没法运行,理解思路最好。
在 html (freemarker 模板)中添加相应的 可视区域div、不可见区域div 和 scroll 事件:
<!-- 存放头部-->
<table id="virtual_header" style="table-layout: fixed; width: 100%">
<colgroup>
<col style="width: 50px; min-width: 50px" />
</colgroup>
<thead>
<tr>
<th
data-index="0"
@mousemove="handleMouseMove"
@mouseout="handleMouseOut"
@mousedown="handleMouseDown"
>
序号
</th>
</tr>
</thead>
<div class="el-table__column-resize-proxy" style="display: none"></div>
</table>
<!-- 存放身体-->
<!-- 可视区域 container -->
<div
class="list_view"
@scroll="handleScroll"
id="list_view"
:style="{ height: '100vh', width: isScoll() ? 'calc(' + scrollAreaWidth() + ' + 8px)' : scrollAreaWidth() }"
>
<!-- 不可见区域 container -->
<div
v-if="useVirtualRender"
class="scroll_area"
:style="{ height: scrollAreaHeight() }"
></div>
<div ref="content_data" class="list_view_content">
<table style="table-layout: fixed">
<!-- 虚拟渲染显示,与头部控制相同 -->
<colgroup>
<col width="50" />
</colgroup>
<tbody>
<tr v-for="(item, index) in pagingData(true)">
<td
align="center"
style="border-left: 1px solid #ebeef5"
:data-index="getPagingDataIndex(index)"
>
getPagingDataIndex(index)
</td>
</tr>
</tbody>
<div class="el-table__column-resize-proxy" style="display: none"></div>
</table>
</div>
</div>
css 设置高度和定位配置:
.list_view {
max-height: 100vh;
overflow: auto;
position: relative;
&::-webkit-scrollbar {
height: 0px;
}
.scroll_area {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list_view_content {
scroll-behavior: smooth; /*滚动平滑属性*/
left: 0;
right: 0;
top: 0;
position: absolute;
}
}
vue (js) 中添加相应的配置和监听函数:
data() {
return {
virtualConfig: {
showNum: 15, //显示几条数据
start: 0, //滚动过程显示的开始索引
itemHeight: 63,
len: 0,
isChangeWidth: false,
}, // 模板配置
list: [] // 表格要显示的数据
}
},
computed: {
// 虚拟列表是否有滚动条, 处理头部高度多 一个滚动条宽度 问题
isScoll() {
return function () {
const elListView = document.getElementById('list_view')
if (elListView) {
const elScrollArea = elListView.firstChild
if (elListView.clientHeight < elScrollArea.clientHeight) {
return true
}
}
return false
}
},
// 内容总高度
scrollAreaHeight() {
const me = this
return function () {
return me.virtualConfig.len * me.virtualConfig.itemHeight + 'px'
}
},
// 内容总宽度
scrollAreaWidth() {
const me = this
return function () {
const elVirtualHeader = document.getElementById('virtual_header')
me.virtualConfig.isChangeWidth = false
if (elVirtualHeader) {
return elVirtualHeader.clientWidth + 'px'
}
return '100%'
}
},
// 前端分页过滤器
pagingData() {
const me = this
return function (isVirtualRender = false) {
// 通过子表数据路径获取分页数据
let list = me.list
if (isVirtualRender) {
// window.addEventListener('resize', () => {
// //窗口改变时
// me.$nextTick(() => {
// me.handleScroll()
// })
// })
me.virtualConfig.len = list.length
let data = list.slice(
me.virtualConfig.start,
me.virtualConfig.start +
me.virtualConfig.showNum
)
return data
} else {
return list
}
}
},
},
methods: {
// 使用虚拟列表渲染后,计算滚动区域的数据索引
computescrollArea(scrollTop, elId) {
scrollTop = scrollTop || 0
const elE = window.document.getElementById(elId)
if (elE) {
let vlopt = this.virtualConfig
// 获取每个item的高度
const tr = elE.querySelector('table > tbody > tr')
if (tr) {
vlopt.itemHeight = tr.getBoundingClientRect().height
}
vlopt.showNum = Math.ceil(elE.clientHeight / vlopt.itemHeight) // 取得可见区域的可见列表项数量
// 开始的数组索引 : 滚到第几条数据 = 滚动高度 / 每个item的高度
let newStart = Math.floor(scrollTop / vlopt.itemHeight)
if (newStart + vlopt.showNum <= vlopt.len) {
vlopt.start = newStart
//绝对定位对相对定位的偏移量 已滚动的高度 = 滚到第几条数据 * 每个item的高度
if (this.$refs['content_data']) {
this.$refs[
'content_data'
].style.webkitTransform = `translate3d(0, ${
vlopt.start * vlopt.itemHeight
}px, 0)`
}
}
}
},
// 子表使用虚拟列表渲染后,滚动触发事件
handleScroll() {
const elE = window.document.getElementById('list_view')
// 获取已滚动的高度
const scrollTop = elE.scrollTop
this.computescrollArea(scrollTop, 'list_view')
},
// 获取虚拟列表的序号
getPagingDataIndex(index) {
return index + this.virtualConfig.start
},
// 子表列宽拖动的鼠标按下事件
handleMouseDown(event) {
// 开始拖拽
this.dragging = true
// 当前拖拽的列所在的表格
let tableEl = event.target
// 当前所在列(单元格)
let thEL = event.target
// ......
const index = thEL.getAttribute('data-index')
const col = tableEl
.querySelector('colgroup')
.getElementsByTagName('col')[index]
thEL.style.width = finalColumnWidth + 'px'
if (col) {
// 虚拟渲染时,需要设置对应的 col 的宽度
col.style.width = thEL.style.width // 以 col 设置为准
}
// ......
// 如果是虚拟列表渲染的话,头和身子是分开的
let listView = tableEl.nextElementSibling
if (listView && listView.classList.contains('list_view')) {
let listTL = listView.querySelector('table')
let listCol = listTL
.querySelector('colgroup')
.getElementsByTagName('col')[index]
if (listCol) {
listCol.style.width = thEL.style.width
}
if (listTL && isChange) {
listTL.style.width = tableEl.style.width
}
this.virtualConfig.isChangeWidth = true
}
// ......
},
}
4、遇到问题:
一、为何没有使用 vue-virtual-scroller 插件?
由于需求虚拟渲染的标签是 tbody
中的 v-for
列表渲染每行数据 tr
,该插件是直接利用 div
进行包裹或者tbody
中包裹 div
,没法实现表格布局的显示,故选择放弃使用该插件。
二、表格头部放到可视区域里的问题?
表格头部放到可视区域,会导致头部跟着一起滚动,而且会闪动,对使用不够友好。由于不能在 tbody
上直接使用 div
标签,会导致表格布局失效,故使用了一个表格实现表头,一个表格实现表身,能够实现表身可以自由 scroll。
三、表头表身分离了,列宽如何同步,横向滚动会造成两个滚动条问题?
1)表头表身的 table
都添加 colgroup
来保证列宽相同,在 colresize
的时候同步设置两部分的 colgroup
中的样式 width
配置。
2)表身有一个 div
(可视区域)包裹,如果拖动列宽操作,可能会导致 此div 出现横向滚动条,另外,包裹 表头和表身的 div
也会出现横向滚动条,则会出现两个滚动条。如下图:
![](https://img.haomeiwen.com/i17273442/bc33a5c599112e38.png)
通过设置 [可视区域div] 宽度等于当前表头的宽度就可以实现 [可视区域div] 的横向滚动条不出现,并且通过外部 div 的横向滚动条可以同步滚动表头表身。
四、表身如果在滚动,那么表身宽度比表头多一个滚动条的宽度, 如何监听是否在滚动?
用可视区域和可滚动区域的高度比较就可以知道是否在滚动状态。
!该文章仅用于学习!
网友评论