虚拟列表

68 阅读1分钟
  • 背景:当后端返回的数据无法分页,且数据量大。前端需要使用虚拟列表技术,减少大量dom节点的渲染。

  • 核心思想:通过计算可视区域范围,动态渲染可见的列表项,避免渲染全部数据。

  • 关键参数

    • containerHeight:容器高度(可视区域)
    • itemHeight:每个列表项的高度(固定或动态)
    • startIndex:起始渲染索引
    • endIndex:结束渲染索引
    • scrollTop:滚动条位置

VirtualList.vue

<template>
  <div 
    class="virtual-list-container" 
    ref="containerRef"
    @scroll="handleScroll"
  >
    <!-- 占位元素,撑开滚动条高度 -->
    <div 
      class="phantom" 
      :style="{ height: totalHeight + 'px' }"
    ></div>
    
    <!-- 实际渲染的列表项 -->
    <div 
      class="list" 
      :style="{ transform: `translateY(${offset}px)` }"
    >
      <div 
        v-for="item in visibleData" 
        :key="item.id" 
        class="list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';

const props = defineProps({
  data: { type: Array, required: true }, // 所有数据
  itemHeight: { type: Number, default: 50 }, // 每项高度(固定高度)
  bufferSize: { type: Number, default: 5 } // 缓冲区大小(额外渲染的项数)
});

const containerRef = ref(null);
const scrollTop = ref(0);

// 计算总高度(撑开滚动条)
const totalHeight = computed(() => props.data.length * props.itemHeight);

// 计算可见区域的起始/结束索引
const startIndex = computed(() => {
  return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.bufferSize);
});

const endIndex = computed(() => {
  return Math.min(
    props.data.length - 1,
    Math.floor((scrollTop.value + containerHeight.value) / props.itemHeight) + props.bufferSize
  );
});

// 容器高度(通过DOM获取)
const containerHeight = ref(0);
onMounted(() => {
  containerHeight.value = containerRef.value?.clientHeight || 0;
});

// 计算偏移量(保证列表项在可视区域内)
const offset = computed(() => {
  return Math.max(0, startIndex.value * props.itemHeight);
});

// 当前可见的数据切片
const visibleData = computed(() => {
  return props.data.slice(startIndex.value, endIndex.value + 1);
});

// 滚动事件处理
const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop;
};
</script>

<style scoped>
.virtual-list-container {
  height: 500px; /* 容器固定高度 */
  overflow-y: auto; /* 启用垂直滚动 */
  position: relative; /* 相对定位 */
  border: 1px solid #eee;
}

.phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1; /* 置于底层 */
}

.list {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.list-item {
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid #f0f0f0;
}
</style>

使用示例

<template>
  <VirtualList :data="listData" :itemHeight="60" />
</template>

<script setup>
import { ref } from 'vue';
import VirtualList from './VirtualList.vue';

// 生成测试数据(30万条)
const listData = ref(
  Array.from({ length: 300000 }, (_, i) => ({
    id: i,
    content: `列表项 ${i + 1}`
  }))
);
</script>