🚀 实现同一个滚动区域包含多个虚拟滚动列表

引言

处理大量数据的列表渲染是一个常见的性能瓶颈。虚拟滚动(Virtual Scrolling)技术通过仅渲染可见区域的内容,显著提升了长列表的性能。本文将介绍如何使用 @tanstack/react-virtual 在同一个滚动区域内实现多个虚拟滚动列表。

利用 @tanstack/react-virtual改造,对历史代码的改造影响范围可控,无心智负担。对一个长列表不同的模块,可以分步完成改造。 demo里展示了两个虚拟列表,如果有更多个虚拟列表的部分,也是适用的。比传统的拼接数据到同一个列表里,此方案更加的快捷能够完成虚拟列表的改造,满足性能、体验要求。

虚拟滚动的原理

虚拟滚动的核心思想是 按需渲染。具体来说:

  1. 计算可见区域:根据滚动容器的滚动位置,计算出当前可见的区域。
  2. 动态渲染内容:仅渲染当前可见区域内的列表项,避免渲染所有数据。
  3. 占位空间:为未渲染的列表项保留占位空间,确保滚动条的行为与完整列表一致。

这样子可以显著减少 DOM 节点的数量,从而提升性能。

@tanstack/react-virtual 简介

@tanstack/react-virtual 是一个轻量级的虚拟滚动库,支持 React 和其他框架。它的主要特点包括:

  • 支持动态高度的列表项。
  • 提供灵活的 API,可以自定义滚动行为。
  • 高性能,适用于大规模数据渲染。

实现多虚拟滚动列表

需求场景

假设我们需要在一个滚动容器内实现两个虚拟列表:

  1. 顶部列表:包含 100 个动态高度的列表项。
  2. 底部列表:包含 300 个动态高度的列表项。

实现步骤

1. 安装依赖

首先,安装必要的依赖:

bash 复制代码
pnpm install @tanstack/react-virtual @faker-js/faker

2. 初始化数据

使用 @faker-js/faker 生成随机数据:

js 复制代码
const randomNumber = (min: number, max: number) =>
  faker.number.int({ min, max });

const sentences = new Array(300)
  .fill(true)
  .map(() => faker.lorem.sentence(randomNumber(20, 70)));

const list = new Array(100).fill(true).map(() => faker.lorem.sentence(randomNumber(20, 70)));

3. 创建滚动容器

定义一个滚动容器,并设置其高度和宽度:

js 复制代码
const virtualizer = useVirtualizer({
  count: sentences.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 45, // 预估高度
  enabled: true,
});

const listVirtualizer = useVirtualizer({
  count: list.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 40, // 预估高度
  enabled: true,
});

4. 实现虚拟滚动

使用 useVirtualizer 创建两个虚拟滚动列表:

js 复制代码
const virtualizer = useVirtualizer({
  count: sentences.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 45, // 预估高度
  enabled: true,
});

const listVirtualizer = useVirtualizer({
  count: list.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 40, // 预估高度
  enabled: true,
});

5. 渲染列表项

动态渲染两个列表的内容

js 复制代码
<div style={{
  height: virtualizer.getTotalSize() + listVirtualizer.getTotalSize(),
}}>
  {/* 顶部列表 */}
  <div style={{
    height: listVirtualizer.getTotalSize(),
    position: "relative",
  }}>
    {listItems.map((virtualRow) => (
      <div
        key={virtualRow.key}
        data-index={virtualRow.index}
        ref={listVirtualizer.measureElement}
        className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
        style={{
          position: "absolute",
          transform: `translateY(${virtualRow.start}px)`,
          width: "100%",
        }}
      >
        <div style={{ padding: "10px 0" }}>
          <div>Row {virtualRow.index}</div>
          <div>{sentences[virtualRow.index]}</div>
        </div>
      </div>
    ))}
  </div>

  {/* 底部列表 */}
  <div style={{
    position: 'relative',
  }}>
    <div style={{
      overflow: 'hidden',
      width: '100%',
      boxSizing: 'border-box',
      transform: `translateY(${(items[0]?.start ?? 0) - height}px)`,
    }}>
      {items.map((virtualRow) => (
        <div
          key={virtualRow.key}
          data-index={virtualRow.index}
          ref={virtualizer.measureElement}
          className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
        >
          <div style={{ padding: "10px 0" }}>
            <div>Row {virtualRow.index}</div>
            <div>{sentences[virtualRow.index]}</div>
          </div>
        </div>
      ))}
    </div>
  </div>
</div>

项目地址

codesandbox.io/p/sandbox/r...

github.com/kevlin-sean...

kevlin-sean.github.io/tanstack_re...

关键点

  1. 共享滚动容器 :两个列表共享同一个滚动容器 ( parentRef )。
  2. 动态高度 :通过 measureElement 动态测量列表项的高度。
  3. 占位空间 :使用 getTotalSize 计算总高度,确保滚动条行为正确。
相关推荐
子兮曰3 小时前
把 649 种文件格式塞进一个网页里:一个「文件不出浏览器」的预览器
前端·javascript·后端
paopaokaka_luck6 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787056 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
IMPYLH7 小时前
HTML 的 <video> 元素
前端·html
溪语流沙7 小时前
【每天一个CSS | Day19】流光文字与一笔写成的手写签名
前端·javascript·css
逐光者9337 小时前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32
To_OC8 小时前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
CDwenhuohuo8 小时前
electron pc项目打包成桌面端
前端·javascript·electron
前端大斗师8 小时前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
Jmyd01238 小时前
用 Web VR 引擎搭建 3D 虚拟展厅:从素材上云到多端实时渲染的完整数据链路
前端·3d·vr·三维数字化