Skip to content

固高虚拟列表实现

核心思路

这个虚拟列表的核心思想是只渲染可视区域内的元素,而不是渲染所有数据项。通过动态计算和更新 DOM 来实现高性能的长列表展示。

关键步骤分解

1. 初始化阶段

  • 容器设置:创建固定高度的容器(600px),设置 overflow-y: auto 启用滚动
  • 状态管理:维护关键状态数据
    • dataSource: 数据源数组
    • itemHeight: 每个列表项的固定高度(100px)
    • viewHeight: 可视区域高度
    • maxCount: 可视区域最多显示的项目数量

2. 核心计算逻辑

  • 起始索引计算:startIndex = Math.floor(scrollTop / itemHeight)
  • 结束索引计算:endIndex = startIndex + maxCount
  • 渲染列表切片:renderList = dataSource.slice(startIndex, endIndex)

3. 视觉欺骗技术

  • 总高度模拟:设置列表容器高度为 dataSource.length * itemHeight,让滚动条显示正确的总长度
  • 位置偏移:使用 transform: translate3d(0, ${startIndex * itemHeight}px, 0) 将可见元素定位到正确位置

4. 性能优化策略

  • RAF 节流:使用 requestAnimationFrame 对滚动事件进行节流处理
  • 变化检测:只有当 startIndex 发生变化时才重新渲染
  • 懒加载:当滚动接近底部时自动加载更多数据

5. 事件处理流程

滚动事件触发 → 计算 startIndex → 检查是否变化 → 重新渲染 → 更新 DOM

实现亮点

  • 固定高度优化:每个项目高度固定为 100px,简化了计算逻辑
  • 缓冲区设计:maxCount 比实际可见数量多 1 个,提供缓冲避免滚动时的闪烁
  • 自动扩容:当滚动到接近底部时自动添加新数据
  • GPU 加速:使用 translate3d 启用硬件加速

适用场景

  • 大量数据的列表展示(如商品列表、聊天记录等)
  • 每个列表项高度相同的场景
  • 需要流畅滚动体验的长列表

这个实现是一个经典的固定高度虚拟列表方案,代码简洁且性能良好。对于更复杂的场景(如动态高度),需要额外的高度缓存和计算逻辑。

完整代码

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      /* 外层容器 - 定义虚拟列表的整体尺寸 */
      .container {
        width: 600px;
        height: 600px;
        margin: 100px auto;
        border: 1px solid red;
      }

      /* 滚动容器 - 提供滚动能力,触发滚动事件 */
      .fs-virtuallist-container {
        width: 100%;
        height: 100%;
        overflow-y: auto; /* 启用垂直滚动 */
      }

      /* 列表容器 - 承载实际的列表项,通过 transform 控制位置 */
      .fs-virtuallist-list {
        width: 100%;
      }

      /* 列表项样式 - 固定高度是虚拟列表计算的基础 */
      .fs-virtuallist-item {
        width: 100%;
        height: 100px; /* 固定高度,简化计算逻辑 */
        box-sizing: border-box;
        border: 1px solid #000;
        text-align: center;
        font-size: 20px;
        line-height: 100px; /* 垂直居中 */
      }
    </style>
  </head>
  <body>
    <!-- 虚拟列表 DOM 结构 -->
    <div class="container">
      <!-- 滚动容器:监听滚动事件,提供可视区域 -->
      <div class="fs-virtuallist-container">
        <!-- 列表容器:通过 JS 动态设置高度和位置,实现虚拟滚动效果 -->
        <div class="fs-virtuallist-list">
          <!-- 列表项将通过 JavaScript 动态生成 -->
        </div>
      </div>
    </div>
    <script>
      /**
       * 虚拟列表类 - 只渲染可视区域内的元素,提升长列表性能
       */
      class FsVirtuallist {
        constructor(containerSelector, listSelector) {
          // 核心状态数据
          this.state = {
            dataSource: [], // 数据源数组
            itemHeight: 100, // 每个列表项的固定高度
            viewHeight: 0, // 可视区域高度
            maxCount: 0, // 可视区域最多显示的项目数量
          };

          // 滚动样式对象,用于控制列表位置和高度
          this.scrollStyle = {};

          // 渲染范围索引
          this.startIndex = 0; // 当前渲染的起始索引
          this.endIndex = 0; // 当前渲染的结束索引
          this.lastStart = -1; // 上次渲染的起始索引,用于变化检测

          // 当前需要渲染的数据切片
          this.renderList = [];

          // DOM 元素引用
          this.oContainer = document.querySelector(containerSelector); // 滚动容器
          this.oList = document.querySelector(listSelector); // 列表容器
        }

        /**
         * 初始化虚拟列表
         */
        init() {
          // 计算可视区域高度
          this.state.viewHeight = this.oContainer.offsetHeight;

          // 计算可视区域最多显示的项目数量(+1 作为缓冲区,避免滚动时闪烁)
          this.state.maxCount = Math.ceil(this.state.viewHeight / this.state.itemHeight) + 1;

          // 绑定滚动事件
          this.bindEvent();

          // 初始化数据
          this.addData();

          // 首次渲染
          this.render();
        }

        /**
         * 绑定滚动事件,使用 RAF 节流优化性能
         */
        bindEvent() {
          this.oContainer.addEventListener("scroll", this.rafThrottle(this.handleScroll.bind(this)));
        }

        /**
         * 计算渲染结束索引
         */
        computedEndIndex() {
          const end = this.startIndex + this.state.maxCount;
          // 确保结束索引不超过数据源长度
          this.endIndex = this.state.dataSource[end] ? end : this.state.dataSource.length;

          // 懒加载:当滚动接近底部时自动加载更多数据
          if (this.endIndex >= this.state.dataSource.length) {
            this.addData();
          }
        }

        /**
         * 计算当前需要渲染的数据切片
         */
        computedRenderList() {
          this.renderList = this.state.dataSource.slice(this.startIndex, this.endIndex);
        }

        /**
         * 计算滚动样式 - 实现视觉欺骗的关键
         */
        computedScrollStyle() {
          const { dataSource, itemHeight } = this.state;
          this.scrollStyle = {
            // 设置列表总高度,减去已滚动过的高度,让滚动条显示正确
            height: `${dataSource.length * itemHeight - this.startIndex * itemHeight}px`,
            // 使用 transform 将可见元素定位到正确位置(GPU 加速)
            transform: `translate3d(0, ${this.startIndex * itemHeight}px, 0)`,
          };
        }

        /**
         * 滚动事件处理函数
         */
        handleScroll() {
          const { scrollTop } = this.oContainer;

          // 根据滚动距离计算起始索引
          this.startIndex = Math.floor(scrollTop / this.state.itemHeight);

          // 只有当起始索引发生变化时才重新渲染(性能优化)
          if (this.startIndex !== this.lastStart) this.render();

          // 记录本次起始索引,用于下次比较
          this.lastStart = this.startIndex;
        }

        /**
         * 渲染函数 - 虚拟列表的核心渲染逻辑
         */
        render() {
          // 1. 计算渲染范围
          this.computedEndIndex();

          // 2. 获取需要渲染的数据切片
          this.computedRenderList();

          // 3. 计算滚动样式
          this.computedScrollStyle();

          // 4. 生成 HTML 模板
          const template = this.renderList.map(i => `<div class="fs-virtuallist-item">${i}</div>`).join("");

          // 5. 更新 DOM
          const { height, transform } = this.scrollStyle;
          this.oList.innerHTML = template; // 更新列表内容
          this.oList.style.height = height; // 设置列表高度
          this.oList.style.transform = transform; // 设置位置偏移
        }

        /**
         * 添加数据 - 模拟数据加载
         */
        addData() {
          // 每次添加 10 条数据
          for (let i = 0; i < 10; i++) {
            this.state.dataSource.push(this.state.dataSource.length + 1);
          }
        }

        /**
         * RAF 节流函数 - 优化滚动性能
         * @param {Function} fn 需要节流的函数
         * @returns {Function} 节流后的函数
         */
        rafThrottle(fn) {
          let lock = false;
          return function (...args) {
            window.requestAnimationFrame(() => {
              // 如果上一帧还在处理中,跳过本次执行
              if (lock) return;
              lock = true;
              fn.apply(this, args);
              lock = false;
            });
          };
        }
      }

      // 创建虚拟列表实例并初始化
      const vList = new FsVirtuallist(".fs-virtuallist-container", ".fs-virtuallist-list");
      vList.init();
    </script>
  </body>
</html>

Released under the MIT License.