盒子的三高总是分不清楚,虚拟列表总是听说却没实际写过。详细学习下做个记录 盒子

image.png

clientHeight 元素可视区高度 content+padding = 102

offsetHeight 元素clientHeight+boreder + 滚动条 = 150

scrollHeight 元素实际高 = 102

scrollTop 滚动上方超出可视区height

虚拟列表

  • 场景:需要展示数千、数万甚至更多条数据(如聊天记录、日志、表格行、商品列表等)。

  • 问题:直接渲染全部数据会导致:

    • DOM节点过多,内存占用高。
    • 布局计算(Reflow)和重绘(Repaint)耗时过长,滚动卡顿。
  • 虚拟列表优势

    • 仅渲染可视区域(如屏幕内)的几十条数据,大幅减少DOM节点。
    • 滚动时动态计算并更新可见内容,保持流畅体验。

代码实现

template 模版

  <div  @scroll="handleScroll" ref="listContainer">
    <!-- 占位元素维持滚动条 -->
    <div class="virtual-list-container" :style="{ height: totalHeight + 'px' }">
      <!-- 渲染可视区域内的列表项 -->
      <div 
        v-for="item in visibleItems" 
        :key="item.index"
        :style="{ transform: `translateY(${item.offset}px)` }
      >
        <!-- 实际列表项内容 --> {{item.data}}
      </div>
    </div>
  </div>
  

基础逻辑

  1. 获取可视区高
  2. 计算可视区域展示数量
  3. 选择数据展示区间
  4. 计算每组列表scrollTop值
  5. 处理滚动事件
<script setup>
import { ref, computed, reactive, onMounted } from 'vue';

// 列表数据源
const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  itemHeight: {
    type: Number,
    required: true
  }
});

// 列表容器引用
const listContainer = ref(null);
// 可视区域项目
const visibleItems = reactive([]
// 滚动位置
const scrollTop = ref(0);

// 总高度(用于占位元素)
const totalHeight = computed(() => {
  return props.items.length * props.itemHeight;
});

// 可视区域高度
const visibleHeight = ref(0);
// 可视区域项目数量
const visibleCount = computed(() => {
  return Math.ceil(visibleHeight.value / props.itemHeight) 
});

// 计算当前可视项目
const visibleItems = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight);
  const end = start + visibleCount.value;
  
  return Array.from({ length: end - start }, (_, i) => {
    const index = start + i;
    const data = props.items[index] || null;
    return {
      index,
      offset: index * props.itemHeight,
      data
    };
  });
});

// 滚动处理
function handleScroll() {
  scrollTop.value = listContainer.value.scrollTop;
}

// 初始化可视区域高度
onMounted(() => {
  visibleHeight.value = listContainer.value.clientHeight;
});

</script>

存在哪些的问题及优化方案

  • 可视区窗口大小发生变化
    • 方案一 window.addEventListener('resize')
      • 监听浏览器窗口尺寸变化
      • 需要配合getBoundingClientRect()offsetHeight等获取高度
      • 用户拖动浏览器会导致频繁触发,无效计算
// 列表容器引用
  const listContainer = ref(null);
  window.addEventListener('resize', () => {
   visibleHeight.value = listContainer.value.clientHeight;
  // 需额外处理节流/防抖
});
  
  • 方案二 resizeObsever
    • 监听指定DOM元素的尺寸变化
    • 可以直接获取元素高度
    • 仅在元素变化是触发回调
    • 浏览器会自动合并回调,减少重排/重绘
// 列表容器引用
const listContainer = ref(null);
const resizeObserver = new ResizeObserver(() => {
    visibleHeight.value = listContainer.value.clientHeight
});
  • 内存泄漏
    • 移除窗口监听
onUnmounted(() => {
  window.removeEventListener('resize', ()=>{...});
  resizeObserver.disconnect();
});
  • 性能问题 滚动卡顿,页面卡死

    -方案: requestAnimationFrame()帧渲染,在浏览器每帧的空余时间进行,避免频繁的重排

function handleScroll() {
  requestAnimationFrame(() => {
    scrollTop.value = listContainer.value.scrollTop;
  });
}
function updateVisibleHeight() {
  requestAnimationFrame(() => {
     visibleHeight.value = listContainer.value.clientHeight;
  });
}
  • 快速滚动出现空白或闪烁

    • 方案:使用缓存区域 ,可以避免重读触发scroll事件
    • 方法:每次加载数据时,头尾多加载几条数据
const  bufferSize  = ref(5)
// 计算当前可视项目
const visibleItems = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight) + bufferSize.value*2;
  const end = start + visibleCount.value - bufferSize;
  ...
});

  • 列表高度不固定
    • 方案: 使用 getBoundingClientRect()获取元素位置信息
<div 
        class="list-item" 
        v-for="item in visibleItems" 
        :key="item.index"
        :data-index="item.index"
        :style="{ transform: `translateY(${item.offset}px)` }"
        @load="measureItemHeight(item.index)" 
      >
const itemHeights = ref([]); // 动态高度缓存
function measureItemHeight(index) {
  const item = document.querySelector(`.list-item[data-index="${index}"]`);
  if (item) {
    itemHeights.value[index] = item.getBoundingClientRect().height;
  }
}
function getOffset(index) {
  return itemHeights.value.slice(0, index).reduce((pre, height) => pre + height, 0);
}
  • -当列表数据(props.items)动态更新时,滚动位置可能错位。
    • 方案 使用watch 监听items
watch(() => props.items, (newItems) => {
  if (newItems.length !== props.items.length) {
    const ratio = newItems.length / props.items.length;
    scrollTop.value *= ratio;
  }
}, { deep: true });
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]