人人都会AI编程

31.5 无限滚动、虚拟列表实现

更新时间:2026-07-11

在长短列表的交互场景中,“一次性把所有数据都渲染出来”是最简单也是最危险的做法。当列表项达到数千甚至数十万条时,DOM 节点的创建、布局和绘制会急剧消耗内存与 CPU,造成页面卡顿甚至崩溃。于是两类优化手段应运而生:无限滚动(解决“加载体验”问题)和虚拟列表(解决“渲染性能”问题)。

31.5.1 无限滚动:按需加载更多数据

无限滚动(Infinite Scroll)的思路很直接:不一次性加载全部数据,而是每次只请求一小段,当用户滚动到接近列表底部时,自动触发下一段数据的加载,将新数据追加到列表中,形成“永远滚不到头”的体验。

核心实现步骤

  1. 监听滚动事件:在列表的滚动容器(可以是 window 或一个固定高度的 div)上绑定 scroll 事件。
  2. 判断是否触底:比较“滚动距离 + 容器可视高度”与“内容总高度”的差值,当差值小于某个阈值(例如 100px)时,认为用户已经接近底部。
  3. 防抖处理scroll 事件触发非常频繁,需要用防抖或节流来控制回调执行频率,避免无谓计算。
  4. 加载状态管理:正在加载时阻止重复请求;所有数据加载完毕时停止监听。

基础代码示例(Vue/React 通用逻辑)

// 滚动容器上的逻辑(以原生 JS 为例)
let isLoading = false;
let currentPage = 1;
let hasMore = true;

const container = document.getElementById('scroll-container');

container.addEventListener('scroll', () => {
  if (isLoading || !hasMore) return;
  
  const { scrollTop, scrollHeight, clientHeight } = container;
  // 距离底部 100px 时触发
  if (scrollHeight - scrollTop - clientHeight < 100) {
    loadMore();
  }
});

async function loadMore() {
  isLoading = true;
  try {
    const newItems = await fetchData(currentPage);
    if (newItems.length === 0) {
      hasMore = false;
      return;
    }
    appendItems(newItems); // 将新数据渲染到列表中
    currentPage++;
  } catch (error) {
    // 处理错误,允许重试
  } finally {
    isLoading = false;
  }
}

注意事项

  • 快速滚动时可能漏触发:一些移动端浏览器在惯性滚动时可能不会持续触发 scroll 事件,可使用 Intersection Observer 作为更可靠的触底检测手段。
  • 数据一致性:如果列表支持筛选、排序,重置状态时要清空列表并从第一页重新加载。
  • SEO 与无障碍:无限滚动通常不利于搜索引擎抓取和辅助技术,对内容型网站需谨慎使用,或提供分页作为降级方案。

31.5.2 虚拟列表:只渲染可视区域内的节点

虚拟列表(Virtual List)解决的是另一个维度的问题:即便所有数据已经在前端(或者单纯数据量巨大),也不可能让成千上万条 DOM 同时存在。虚拟列表的核心思想是按需渲染——只创建当前视口内可见的那些列表项,其余部分用空白占位来维持滚动条高度。

核心原理拆解

  1. 定高与不定高
  • 固定高度:每项高度一致,实现最为简单。
  • 动态高度:每项高度不固定,需要提前缓存或预估高度,并在渲染后进行修正,实现更复杂。
  1. 可视区域计算
  • scrollTop:容器已滚动的距离。
  • containerHeight:容器可视高度。
  • itemHeight:单项高度(固定高度可用,动态需累加缓存)。

通过 startIndex = Math.floor(scrollTop / itemHeight)endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight) 确定当前应渲染的起止索引。

  1. 偏移量与占位
  • 列表整体高度 = 总数据量 × 单项高度(或动态累加值),用一个占位 div 撑开滚动条。
  • 可见项通过 transform: translateY(startIndex * itemHeight)padding-top 定位到正确位置。

固定高度虚拟列表简化实现(Vue 3 组合式)

<template>
  <div class="virtual-container" 
       ref="containerRef" 
       @scroll="onScroll"
       :style="{ height: containerHeight + 'px', overflow: 'auto' }">
    <!-- 占位元素,设定总高度 -->
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <!-- 可见项容器,通过偏移定位 -->
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div v-for="item in visibleItems" 
             :key="item.id" 
             :style="{ height: itemHeight + 'px' }">
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

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

const props = defineProps({
  list: Array,
  itemHeight: Number,
  containerHeight: Number
});

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

const totalHeight = computed(() => props.list.length * props.itemHeight);

const startIndex = computed(() => 
  Math.floor(scrollTop.value / props.itemHeight)
);

const visibleCount = computed(() => 
  Math.ceil(props.containerHeight / props.itemHeight) + 1 // 额外多渲染一项做缓冲
);

const visibleItems = computed(() => 
  props.list.slice(startIndex.value, startIndex.value + visibleCount.value)
);

const offsetY = computed(() => 
  startIndex.value * props.itemHeight
);

function onScroll() {
  scrollTop.value = containerRef.value.scrollTop;
}
</script>

动态高度虚拟列表的难点

  • 需要维护一个存储每项预估高度、实际高度及累计偏移量的数组。
  • 当某项渲染完成后,通过 ResizeObservernextTick 获取其真实高度并更新缓存,同时重新计算后面所有项的偏移量。
  • 滚动时通过二分查找在累积偏移量数组中找到 startIndex,效率需保证。

工程中一般不会手动从头写,成熟的库如 vue-virtual-scrollerreact-windowreact-virtualized 等已经完美处理了固定/动态高度、横向/纵向、网格等多种场景。

31.5.3 无限滚动 + 虚拟列表的协同

在实际业务中,无限滚动和虚拟列表常常需要组合使用:用无限滚动分页请求数据,用虚拟列表保证已加载数据的高性能渲染。关键是处理好以下交互:

  • 触底加载新数据后,需要平滑地将新数据追加到数据池(而不是替换整个数组),虚拟列表自动感知数据长度变化,保持当前滚动位置不跳动。
  • 如果数据总量尚未确定(如搜索结果),虚拟列表的总高度需要动态增长,占位元素的高度要随数据累积而更新。
  • 加载中的 loading 指示器可以放置在列表底部,作为最后一行的特殊渲染项,同时避开虚拟列表的切片逻辑。

31.5.4 避坑要点

  • 滚动位置抖动:数据加载后追加到尾部容易保持位置,但在顶部插入数据(如聊天消息的上拉加载)会导致内容向下偏移,需手动调整 scrollTop
  • 快速滑动白屏:如果可视区域内的节点来不及渲染,用户会看到短暂的白屏。增大渲染缓冲区域(上下各多渲染几个额外项)可以有效缓解。
  • 列表项内有交互或表单:虚拟列表在滚动时是会销毁和创建 DOM 节点的,如果项内有未保存的状态(如输入框内容、展开/折叠状态),状态会丢失。需要将这些状态提升到列表数据中,用“状态外置”的方式保存。
  • 避免频繁触发 scroll 计算:采用 requestAnimationFrame 或框架带来的 onScroll 节流机制,确保更新速率与屏幕刷新率同步。

虚拟列表和无限滚动看似是两个独立技巧,实际在长列表场景中互补成为一对黄金搭档。掌握了它们,你就可以在看似不可能的数据量下,依然给用户提供丝滑的浏览体验。