虚拟列表正确打开方式

3 阅读6分钟

1. 虚拟列表

1.1. 虚拟列表的原理?

  • 虚拟列表是一种优化长列表渲染性能的技术,通过只渲染可视区域的内容,而不是整个列表,从而提高渲染性能和用户体验

1.2. 为什么需要虚拟列表?

  • 性能瓶颈:
    • 数据量过大时,直接渲染所有DOM 节点会导致页面性能下降,甚至出现卡顿或者崩溃现象。
    • 包括浏览器的重绘和重排可能都是会造成性能问题的
  • 解决目标:
  • 只渲染可视区域的内容 动态复用不可见的DOM元素

1.3. 虚拟列表的核心思想

  • 实现方式
    • 仅渲染可视区域的内容
      • 列表中不可见的部分不会生成对应的 DOM 节点
    • 动态复用不可见的 DOM 元素
      • 当列表滚动时,只更新可视区域的 DOM 节点,而不是重新渲染整个列表。
    • 占位元素
      • 通过一个大的占位容器,确保滚动条的正常使用,在列表的顶部和底部添加占位元素,用于计算可视区域的起始索引和结束索引。

1.4. 实现步骤

1.4.1. 确定可见区域的的高度和可渲染的数量

  • 假设容器高度为 containerHeight
  • 每个列表项的固定高度为 itemHeight
  • 可视区域内可以显示的列表数量为
let visibleCount = Math.ceil(containerHeight / itemHeight);
  • 渲染的额外缓冲区数量为 buffer, 保证滚动的流畅度
let renderCount = visibleCount + 2 * buffer

1.4.2. 动态计算开始渲染的起点

  • 根据滚动位置计算可见列表的其实索引 startIndex
let startIndex = Math.floor(scrollTop / itemHeight);
  • 根据 startIndexrenderCount,可以确定需要渲染的列表项索引范围
let endIndex = startIndex + renderCount - 1;

1.4.3. 占位元素

  • 创建一个占位元素,它的高度等于整个列表的高度
let totalHeight = totalItemCount * itemHeight;

通过设置占位元素的高度,确保滚动条正常工作

1.4.4. 渲染可见区域的内容

  • 通过偏移量 startIndex 对应元素的位置
let offsetY = startIndex * itemHeight;

1.5. 原生代码实现

1.5.1. HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>虚拟列表</title>
    <style>
      #container {
        position: relative; /* 新增定位上下文 */
        height: 400px;
        overflow-y: auto;
        border: 1px solid #000;
      }
      #list {
        position: absolute; /* 脱离文档流 */
        top: 0;
        left: 0;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="container" >
      <div id="placeholder"></div>
      <div id="list"></div>
    </div>
    <script src="./VirtualList.js"></script>
  </body>
</html>

1.5.2. JavaScript

const container = document.getElementById('container'); // 容器
const placeholder = document.getElementById('placeholder'); // 占位符
const list = document.getElementById('list'); // 列表

const totalItems = 10000; // 总数据量
const itemHeight = 30; // 每个数据项的高度
const containerHeight = container.clientHeight; // 容器高度
const visibleItems = Math.ceil(containerHeight / itemHeight); // 可见数据项数量
const buffer = 5; // 缓冲区数量
const renderCount = visibleItems + buffer * 2; // 渲染数据项数量

// 模式生成10000个数据
const data = Array.from({ length: totalItems }, (_, index) => ({
  id: index,
  value: `Item ${index}`,
}));

// 设置占位容器的高度
placeholder.style.height = `${totalItems * itemHeight}px`;

// 渲染可视区域列表
function renderVirtualList(scrollTop) {
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
  const endIndex = Math.min(totalItems -1, startIndex + renderCount - 1);

  // 计算偏移量
  const offsetY = startIndex * itemHeight;
  list.style.transform = `translateY(${offsetY}px)`;

  // 渲染可见内容
  list.innerHTML = data.slice(startIndex, endIndex + 1).map(item => `<div style="color: red; height: ${itemHeight}px">${item.value}</div>`).join('');
}

// 初始化渲染
container.addEventListener('scroll', () => {
  const scrollTop = container.scrollTop;
  renderVirtualList(scrollTop);
});
renderVirtualList(0);

1.6. 核心原理解析

1.6.1. 滚动事件驱动

  • 监听滚动事件,计算当前的 scrollTop, 动态确定需要渲染的 startIndexendIndex
  • 滚动到新位置时,更新可视区域的 DOM 内容。

1.6.2. 占位容器

  • 占位元素通过设置总高度,维持滚动条的正确性。
  • 真实 DOM 仅渲染可见部分,降低 DOM 节点数量

1.6.3. DOM 元素复用

  • 滚动时并不会移除和新建大量 DOM 节点,而是复用现有 DOM,动态更新内容

1.7. 优化

1.7.1. 优化点 1 DOM 渲染延迟 出现白屏现象

1.7.1.1. 问题描述
  • 当用户快速滚动的时候 scroll 事件频繁触发,浏览器可能无法及时完成 DOM 的更新,导致用户看到空白区域或闪烁
1.7.1.2. 原因
  • 渲染滞后:快速滚动时,scroll 事件触发评率过高,渲染任务无法跟上滚动的速度。
  • 计算复杂: 每次滚动都需要重新极端可见区域的索引范围,并生成新的 DOM 内容
1.7.1.3. 解决方案 1 节流 | 防抖
let ticking = false;
container.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      renderVirtualList(container.scrollTop);
      ticking = false;
    });
    ticking = true;
  }
});
  • 使用 requestAnimationFrame throttle 降低 scroll 事件触发的频率
1.7.1.4. 解决方案 2 预渲染
  • 渲染当前可见范围之外的额外缓冲区域(如上下各增加 2-3 屏),减少快速滚动时的白屏问题
  • 简单说就是 提前多渲染一部分内容 做为缓冲区

1.7.2. 优化点 2 滚动跳跃(滚动不流畅)

1.7.2.1. 问题描述
  • 快速滚动时,可能出现内容突然跳跃或位置错乱的情况,尤其是在列表项高度动态变化的情况下
1.7.2.2. 原因
  1. 列表项高度动态变化:虚拟列表通常假设列表项高度固定,若高度动态变化,会导致滚动位置计算错误。
  2. 滚动位置重置:快速滑动时,渲染延迟或者数据更新不同步,可能导致滚动位置被重置。
1.7.2.3. 解决方案 1 固定高度
  • 确保列表项的高度固定,避免动态高度。 提前计算要渲染项列表每项的高度进行累加
  • 如果高度动态变化,提前计算每个项的高度,使用累加值管理滚动位置。
const itemHeights = [....30, 40, 50]; // 每一项的高度
const offsetY = itemHeights.slice(0, startIndex).reduce((sum, h) => sum + h, 0)
1.7.2.4. 解决方案 2 缓存布局
  • 对于高度动态的场景,提前缓存已计算的高度,减少重复计算。
const heightCache = new Map();
function getItemHeight(index) {
  if (!heightCache.has(index)) {
    const height = calculateHeight(index); // 动态计算高度
    heightCache.set(index, height);
  }
  return heightCache.get(index);
}
1.7.2.5. 解决方案 3 平滑滚动
  • 在渲染时保持视图与滚动位置同步,避免视觉上的跳跃。

1.7.3. 优化点 3 首屏渲染卡顿

1.7.3.1. 问题描述
  • 当虚拟列表初始化时,如果数据量非常大(如上万条),可能会导致首屏渲染时间较长,用户体验不佳
1.7.3.2. 原因
  • 初始化计算开销大:需要计算整个列表的占位高度,并生成首屏可见的 DOM 节点。
  • 占位 DOM 的创建:虚拟列表会创建一个大的占位元素,初始化开销较高
1.7.3.3. 解决方案 1 延迟加载
  • 初始化时仅加载首屏内容,其他部分可以异步加载
setTimeout(() => {
  renderVirtualList(container.scrollTop);
}, 5)
1.7.3.4. 解决方案 2 异步数据分批渲染
  • 对数据进行分批加载而不是一次性加载完
const batchSize = 100;
for (let i = 0; i < totalItems; i += batchSize) {
  setTimeout(() => renderBatch(i, i + batchSize), 0);
}

1.8. 其他方式

  • 还有很多便捷的方式 就是借助三方插件库

1.9. 总结

1.9.1. 快速滑动常见问题

  1. DOM 渲染延迟:导致白屏或闪烁。
  2. 动跳跃:由于动态高度或同步问题导致位置错乱。
  3. 首屏卡顿:初始化计算过多。
  4. 滚动条错位:占位高度不准确或不同步。
  5. 内存占用过大:极大数据量导致性能瓶颈