You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Virtual Scroll实现异常:向下滚动出现无限滚动问题

虚拟滚动无限滚动问题排查与修复

问题现象

纯JavaScript实现虚拟滚动时,向下滚动会触发无限滚动,直接滑到底部无法正常定位。实现方案是通过top和bottom两个容器模拟滚动空间,滚动时动态调整二者高度并重新渲染可见内容。

原实现代码

JavaScript

class VirtualScroll {
  constructor(selector, visibleRows = 0, amount, rowHeight) {
    this.scrollContainer = document.querySelector(selector);

    if (!this.scrollContainer) {
      return false;
    }

    this.rowHeight = rowHeight;
    this.visibleRows = visibleRows;
    this.start = 0;

    this.data = this.createTableData(amount);

    // 顶部滚动空间
    this.topSpace = document.createElement("div");
    this.topSpace.classList.add('hidden-top');
    this.topSpace.style = `height: ${this.getTopHeight()}px;`;
    this.scrollContainer.appendChild(this.topSpace);

    // 创建可见内容容器
    this.container = document.createElement("div");
    this.container.classList.add('scroll-container');
    this.fillContainer();

    this.scrollContainer.appendChild(this.container);

    // 底部滚动空间
    this.bottomSpace = document.createElement("div");
    this.bottomSpace.classList.add('hidden-bottom');
    this.bottomSpace.style = `height: ${this.getBottomHeight()}px`
    this.scrollContainer.appendChild(this.bottomSpace);

    this.scrollContainer.addEventListener('scroll', (e) => {
      const oldStart = this.start + 0;
      this.start = Math.min(
        this.data.length - this.visibleRows - 1,
        Math.floor(e.target.scrollTop / this.rowHeight)
      )

      if (oldStart === this.start) {
        return false;
      }

      this.bottomSpace.style = `height: ${this.getBottomHeight()}px`;
      this.topSpace.style = `height: ${this.getTopHeight()}px;`;
      this.fillContainer();
    });
  }

  getTopHeight() {
    return this.rowHeight * this.start;
  }

  getBottomHeight() {
    return this.rowHeight * (this.data.length - (this.start + this.visibleRows + 1));
  }

  createTableData(h) {
    return new Array(h).fill(0).map((_, row) => {
      return row;
    });
  }

  // 填充可见内容
  fillContainer() {
    this.container.innerHTML = "";
    this.data.slice(this.start, this.start + this.visibleRows + 1).map((row, rowIndex) => {
      const item = document.createElement("div");
      item.classList.add('scroll-item');
      item.innerText = row;
      item.id = this.start + ' ' + rowIndex + row;
      this.container.appendChild(item);
    })
  }
}

new VirtualScroll(".container", 4, 100, 30);

CSS

* {
  box-sizing: border-box;
}

.container {
  height: 120px;
  overflow-y: auto;
}

.scroll-container {
  width: 100%;
}

.scroll-item {
  height: 30px;
  border: 1px solid black;
  padding: 10px;
  color: black;
}

HTML

<div class="container"> </div>

问题根源

  1. 底部空间高度计算错误:
    getBottomHeight方法中data.length - (this.start + this.visibleRows + 1)多减了1,导致底部容器高度小于实际需要的高度。滚动时,滚动容器总高度突然变小,浏览器会自动调整滚动位置,进而再次触发scroll事件,形成循环直至滑到底部。

  2. 起始行边界值错误:
    scroll事件中this.data.length - this.visibleRows - 1的-1多余,导致start的最大值被限制过小,使得可见区域覆盖的行数不足,间接影响了滚动容器的总高度计算,加剧了滚动异常。

修复方案

修复后的JavaScript代码

class VirtualScroll {
  constructor(selector, visibleRows = 0, amount, rowHeight) {
    this.scrollContainer = document.querySelector(selector);

    if (!this.scrollContainer) {
      return false;
    }

    this.rowHeight = rowHeight;
    this.visibleRows = visibleRows;
    this.start = 0;

    this.data = this.createTableData(amount);

    // 顶部滚动空间
    this.topSpace = document.createElement("div");
    this.topSpace.classList.add('hidden-top');
    this.topSpace.style = `height: ${this.getTopHeight()}px;`;
    this.scrollContainer.appendChild(this.topSpace);

    // 创建可见内容容器
    this.container = document.createElement("div");
    this.container.classList.add('scroll-container');
    this.fillContainer();

    this.scrollContainer.appendChild(this.container);

    // 底部滚动空间
    this.bottomSpace = document.createElement("div");
    this.bottomSpace.classList.add('hidden-bottom');
    this.bottomSpace.style = `height: ${this.getBottomHeight()}px`
    this.scrollContainer.appendChild(this.bottomSpace);

    this.scrollContainer.addEventListener('scroll', (e) => {
      const oldStart = this.start + 0;
      // 修正start的边界值,移除多余的-1
      this.start = Math.min(
        this.data.length - this.visibleRows,
        Math.floor(e.target.scrollTop / this.rowHeight)
      )

      if (oldStart === this.start) {
        return false;
      }

      this.bottomSpace.style = `height: ${this.getBottomHeight()}px`;
      this.topSpace.style = `height: ${this.getTopHeight()}px;`;
      this.fillContainer();
    });
  }

  getTopHeight() {
    return this.rowHeight * this.start;
  }

  // 修正底部高度计算,移除多余的-1
  getBottomHeight() {
    return this.rowHeight * (this.data.length - (this.start + this.visibleRows));
  }

  createTableData(h) {
    return new Array(h).fill(0).map((_, row) => {
      return row;
    });
  }

  // 填充可见内容
  fillContainer() {
    this.container.innerHTML = "";
    this.data.slice(this.start, this.start + this.visibleRows + 1).map((row, rowIndex) => {
      const item = document.createElement("div");
      item.classList.add('scroll-item');
      item.innerText = row;
      item.id = this.start + ' ' + rowIndex + row;
      this.container.appendChild(item);
    })
  }
}

new VirtualScroll(".container", 4, 100, 30);

修复说明

  • 调整getBottomHeight的计算逻辑,确保底部空间高度等于总数据行数减去当前可见区域覆盖的行数乘以行高,保证滚动容器的总高度始终与真实数据总高度一致。
  • 修正start的边界值,将最大值改为data.length - visibleRows,确保start + visibleRows能覆盖到最后一行数据,避免高度计算偏差和内容截断。

内容的提问来源于stack exchange,提问作者Serj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 20:14:57