为什么需要使用虚拟列表?

为了处理一次渲染多个数据,而造成页面卡顿的现象。

什么是虚拟列表?

虚拟列表就是按需显示的一种实现,即只对可见区域进行渲染

实现他需要什么东西呢?

  • 计算当前可视区域起始数据索引startIndex
  • 计算当前可视区域结束数据索引endIndex
  • 计算当前可是区域的数据,并渲染到页面中
  • 计算startIndex对应数据在整个列表中的偏移位置startOffset并设置到列表上

讲需要展示数据的设置为如下结构

<div class="virtual-list-container">
    <div class="virtual-list-phantom"></div>
    <div class="virtual-list">
        <div class="virtual-item"></div>
    </div>
</div>

virtual-list-container是可视区域的容器 virtual-list-phantom为容器的占位,高度为总列表的高度,用于形成滚动条。 virtual-list 为列表项的渲染区域 需要监听virtual-list-container的滚动事件,获取滚动位置scrollTop 然后让virtual-list向上偏移,偏移量为:startOffset(scrollTop-(scrollTop%itemSize)) 可视区域高度为screenHeight 列表每一项高度固定,为itemSize 列表数据为listData 当前滚动位置称之为scrollTop

  • 列表总高度listHeight = listData.length * itemSize
  • 可现实列表项,visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的其实索引startIndex = Math.floor(scrollTop/itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

偏移量startOffset = scrollTop - (scrollTop % itemSize)

简易代码如下

<template>
    <div class="virtual-list-container" ref="container" @scroll="onScroll">
        <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
        <div class="virtual-list" :style="{ transform: getTransform }">
            <div class="virtual-list-item" v-for="(item, index) in visibleData" :key="index"
                :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }">
                {{ item }}
            </div>
        </div>
    </div>
</template>



<script setup lang="ts">
import { ref, computed, toRefs, onMounted, nextTick } from "vue";
const screenHeight = ref(0);
const startIndex = ref(0);
const endIndex = ref(0);
const startOffset = ref(0);

onMounted(() => {
    screenHeight.value = container.value.clientHeight;
    startIndex.value = 0;
    endIndex.value = startIndex.value + visibleCount.value;
    startOffset.value = startIndex.value * itemSize.value;
});
const props = defineProps({
    listData: {
        type: Array<any>,
        default: () => [],
    },
    itemSize: {
        type: Number,
        defualt: 60,
    },
});

const { listData, itemSize } = toRefs(props);
const container = ref();
const listHeight = computed(() => listData.value.length * itemSize.value);
const visibleCount = computed(() =>
    Math.ceil(screenHeight.value / itemSize.value)
);

const getTransform = computed(
    () => `translate3d(0, ${startOffset.value}px, 0)`
);

const visibleData = computed(() =>
    listData.value.slice(startIndex.value, endIndex.value)
);

const onScroll = (event: Event) => {
    let scrollTop = container.value.scrollTop;
    startIndex.value = Math.floor(scrollTop / itemSize.value);
    endIndex.value = startIndex.value + visibleCount.value;
    startOffset.value = scrollTop - (scrollTop % itemSize.value);
    //   console.log("startIndex", startIndex.value, "endIndex", endIndex.value);\
    nextTick(() => {
        console.log("visibleData", visibleData.value);
    });
};

</script>



<style scoped>
.virtual-list-container {
    width: 300px;
    height: 600px;
    overflow: auto;
    position: relative;
    -webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
}

.virtual-list {
    left: 0;
    right: 0;
    top: 0;
    position: absolute;
    text-align: center;
}

.virtual-list-item {
    padding: 10px;
    color: #555;
    box-sizing: border-box;
    border-bottom: 1px solid #999;
    width: 100px;
}
</style>

要设置infinite-list-phantom的高度为整个列表的高度,然后将其隐藏

对于这个虚拟列表监听滚动事件。 可以使用防抖,来进行一个优化,防止多次触发。 或者使用IntersectionObserver