vue3高度不固定定使用虚拟列表展示数据(列表内容高度不固定)
公司代码优化发现因为数据量太多,造成操作加载页面非常卡顿,决定使用虚拟加载的方法进行优化,发现网上的虚拟列表插件只能使用固定高度,查了好久发现没法偷懒,自己写了一个
注意点:
(1)在html中,cell使用了绝对定位,定位的top值是由positionData中的top值来决定的,所以根据positionData数组中的top值,html也会跟着进行位置的重新定位
(2)盒子的滚动区域高度是由positionData数组的最后一项数据的boxHeight属性决定的,在开始时,只有预估的高度,所以盒子的滚动高度也是预估的。在positionData数组重新校对后,数组的每一项boxHeight也跟着校对,所以滚动盒子的真实高度也会根据实际的cell盒子高度变化而变化
1.html 下列各个属性必填,不然会出问题
//下列各个属性必填,不然会出问题
<div
class="ruleDataList"
ref="wrapper"
@scroll="wrapperScroll($event)"
>
<div
class="contentBox"
:style="{ height: contentBoxHeight + 'px' }"
>
<div
ref="cellNode"
class="cell"
v-for="(item, index) in showList"
:style="{ top: positionData[item.renderIndex].top + 'px' }"
:data-index="item.renderIndex"
:key="item.rule_id"
>
..............
</div>
</div>
</div>
2.css
.ruleDataList{
height: calc(100vh - 406px);
overflow: auto;
.contentBox {
position: relative;
width: 100%;
}
.cell {
position: absolute;
left: 0;
width: 100%;
}
}
- js方法
在页面渲染后先获取盒子高度containerDomHeight.value = wrapper.value.clientHeight;(一定要在数据渲染出来以后)
在调用方法 initvVirtual()
// 展示的数据
const showList = ref([
// {
// index, // 位置索引
// data: '暂无数据'
// }
]);
// 盒子高度
const containerDomHeight = ref(0);
const wrapper = ref(null);
const cellNode = ref(null);
// 每行高度(预估高度)
let cellHeight = ref(500);
// 可视框显示的数据量
const showCount = ref(10);
// 需要滚动的内容高度
const contentBoxHeight = computed(() => {
return positionData.value[positionData.value.length - 1]?.boxHeight;
});
// 存储每一项的位置信息
const positionData = ref([
// {
// index: 0, // 位置索引
// top: 0, // 距离顶部的位置
// height: 0, // 当前项的高度
// // 顶部的位置 + 当前项的高度 = 滚动盒子应用的高度
// boxHeight: 0
// }
]);
// 初始化数据方法
function initvVirtual() {
// 可视框需要显示的数据量
showCount.value = Math.ceil(containerDomHeight.value / cellHeight.value);
// 预估的位置信息
positionData.value = ruleSeverityData.value.map((item, index) => {
return {
index: index,
top: cellHeight.value * index,
height: cellHeight.value,
// 当前项为最后一条数据,滚动盒子的预估高度
// top值加上本身的高度,就能得知滚动盒子的预估高度
// boxHeight: index * cellHeight + cellHeight
boxHeight: (index + 1) * cellHeight.value
};
});
renderData();
}
//监听滚动事件
const wrapperScroll = e => {
renderData();
};
// 获取需要渲染数据,默认渲染数据
function renderData() {
// 滚动出去的距离
let scrollTop = wrapper.value.scrollTop;
// 可视区域第一行索引
let startIndex = Math.max(getStartIndex(scrollTop) - 2, 0);
// 可视区域最后一行索引
let endIndex = Math.min(
startIndex + showCount.value + 4,
ruleSeverityData.value.length
);
// 获取需要渲染的数据
showList.value = [];
for (let i = startIndex; i < endIndex; i++) {
ruleSeverityData.value[i].renderIndex = positionData.value[i].index;
showList.value.push(ruleSeverityData.value[i]);
}
nextTick(() => {
if (cellNode.value) {
cellNode.value.forEach(node => {
// 实际高度
let height = node.getBoundingClientRect().height;
let index = +node.dataset.index;
let position = positionData.value[index];
let cha = height - position.height;
// console.log(cha)
// 当前node的实际高度,跟预估高度不一致
if (cha) {
// 更新当前node的位置信息
positionData.value[index].height = height;
positionData.value[index].boxHeight += cha;
for (let i = index + 1; i < positionData.value.length; i++) {
// 更新当前node后面所有项的位置信息
positionData.value[i].top = positionData.value[i - 1].boxHeight;
positionData.value[i].boxHeight += cha;
}
}
});
}
});
}
// 获取可视区域第一行索引
function getStartIndex(scrollTop) {
// 暴力搜索
// let startIndex = this.positionData.findIndex(item => {
// return item.boxHeight > scrollTop
// })
// 二分查找
let left = 0,
right = positionData.value.length - 1,
res = right;
while (left < right) {
let mid = Math.floor((right - left) / 2) + left;
if (positionData.value[mid].boxHeight === scrollTop) {
// console.log(mid)
return mid + 1;
} else if (positionData.value[mid].boxHeight > scrollTop) {
res = Math.min(mid, res);
right = right - 1;
} else if (positionData.value[mid].boxHeight < scrollTop) {
left = mid + 1;
}
}
return res;
}