virtual-list-common

A Vue and React universal virtual list for efficiently rendering large lists with large data, only rendering elements in the visible area and reusing DOM.
2,677
Install
npm install virtual-list-common
Latest Version:0.7.1
Vue:3
License:ISC
Last Updated:Aug 11, 2026
Links: npm
Maintainer: admitor

virtual-list-common 虚拟列表

npm version license

Vue 3 / React 通用虚拟列表,高效渲染海量数据。仅渲染可见区域的 DOM 并借助元素池复用,确保大数据场景下丝滑流畅。


目录


特性

  • 框架无关核心 + 原生组件 — 核心类零框架依赖;同时提供 Vue 3 和 React 原生组件封装
  • DOM 池化复用 — 自适应元素池动态调整大小(空闲收缩,高需求增长),万级数据依然 60fps
  • 双向滚动 — vertical / horizontal,支持多列 / 多行布局
  • 无限滚动 — 内置加载更多、加载指示器、结束提示
  • 自适应高度 — 基于 ResizeObserver 的动态行高(垂直模式)
  • 响应式布局 — 窗口缩放 / 容器尺寸变化自动重算,无需手动介入
  • 图片懒加载 + 预加载 — IntersectionObserver 懒加载 + 可插拔预加载管理器,并发控制、超时降级
  • 滚动适配器 — 默认原生滚动、可选 Lenis 平滑滚动、可注入任意自定义 ScrollAdapter
  • 完整 TypeScript 类型 — 泛型数据源、自动生成的 .d.ts
  • Tree-shakable — ESM 输出,sideEffects: false
  • 瀑布流布局 — 项目自动落入最短列,形成参差不齐的瀑布效果
  • 分组支持 — 数据按分组组织,每组包含标题头和下属数据项

安装

npm install virtual-list-common
# 或
pnpm add virtual-list-common
# 或
yarn add virtual-list-common

快速开始

方式一:使用框架原生组件(推荐)

从 v0.1.0 开始,提供了 Vue3 和 React 的原生组件支持,使用更加便捷。

Vue 3 组件
<template>
  <VirtualList
    :items="items"
    :itemHeight="310"
    :itemWidth="200"
    :columns="2"
    height="600px"
    ref="virtualListRef"
    :renderItem="renderItem"
  >
  </VirtualList>
</template>

<script setup>
import { ref } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';

const items = ref([
  'https://picsum.photos/200/300?random=1',
  'https://picsum.photos/200/300?random=2',
  // ...
]);

const virtualListRef = ref(null);

// 自定义渲染函数 — 返回 Vue VNode(推荐)
const renderItem = (index, itemData) => {
  return h('div', { class: 'custom-item' }, [
    h('img', { src: itemData, alt: `Item ${index}` }),
    h('span', {}, `Item ${index}`)
  ]);
};

// 或返回 HTMLElement(向后兼容)
const renderItem = (index, itemData) => {
  const div = document.createElement('div');
  div.className = 'custom-item';
  div.innerHTML = `<span>Item ${index}</span>`;
  return div;
};

</script>
React 组件
import { useRef, useState } from 'react';
import { VirtualListReact as VirtualList } from 'virtual-list-common/react';

function App() {
  const [items] = useState([
    'https://picsum.photos/200/300?random=1',
    'https://picsum.photos/200/300?random=2',
    // ...
  ]);

  const virtualListRef = useRef(null);

  // 自定义渲染函数 — 返回 ReactElement(JSX)
  const renderItem = (index, itemData) => (
    <div className="custom-item">
      <img src={itemData} alt={`Item ${index}`} />
      <span>Item {index}</span>
    </div>
  );

  return (
    <VirtualList
      ref={virtualListRef}
      items={items}
      itemHeight={310}
      itemWidth={200}
      columns={2}
      height="600px"
      renderItem={renderItem}
    />
  );
}
Slot / 插槽支持(推荐)

除了使用 renderItem prop 外,还可以使用框架原生的插槽机制来渲染列表项。使用插槽更加直观,且符合框架的使用习惯。插槽优先级高于 renderItem prop,当同时提供时插槽会优先生效。

Vue 3 Scoped Slot(作用域插槽)

通过 Vue 的默认作用域插槽渲染每一项,插槽参数为 { index, itemData }:

<template>
  <VirtualList
    :items="items"
    :itemHeight="310"
    :itemWidth="200"
    :columns="2"
    height="600px"
    ref="virtualListRef"
  >
    <template #default="{ index, itemData }">
      <div class="custom-item">
        
        <span>Item {{ index }}</span>
      </div>
    </template>
  </VirtualList>
</template>

<script setup>
import { ref } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';

const items = ref([
  'https://picsum.photos/200/300?random=1',
  'https://picsum.photos/200/300?random=2',
  // ...
]);

const virtualListRef = ref(null);

</script>
React Render Prop(children 作为渲染函数)

React 中通过 children 属性传入渲染函数(render prop 模式):

import { useRef, useState } from 'react';
import { VirtualListReact as VirtualList } from 'virtual-list-common/react';

function App() {
  const [items] = useState([
    'https://picsum.photos/200/300?random=1',
    'https://picsum.photos/200/300?random=2',
    // ...
  ]);

  const virtualListRef = useRef(null);

  return (
    <VirtualList
      ref={virtualListRef}
      items={items}
      itemHeight={310}
      itemWidth={200}
      columns={2}
      height="600px"
    >
      {(index, itemData) => (
        <div className="custom-item">
          <img src={itemData} alt={`Item ${index}`} />
          <span>Item {index}</span>
        </div>
      )}
    </VirtualList>
  );
}

注意:当同时提供插槽/children 和 renderItem prop 时,插槽/children 会优先生效,renderItem 将被忽略。控制台会输出一条 warning 提示。


方式二:使用核心类(高级用法)

如果你需要更底层的控制,可以直接使用 VirtualList 核心类。

核心类基础用法
import VirtualList from 'virtual-list-common';

const container = document.getElementById('container');
const items = [
  'https://picsum.photos/200/300?random=1',
  'https://picsum.photos/200/300?random=2',
  // ...
];

const virtualList = new VirtualList(container, items, {
    itemHeight: 310,
    itemWidth: 200,
    columns: 2
});
自定义渲染(使用核心类)
// Vue 中使用核心类
import { createApp, h } from 'vue';
import VirtualList from 'virtual-list-common';
import MyVueComponent from './MyVueComponent.vue';

const virtualList = new VirtualList(container, items, {
    onRenderItem: (index, itemData) => {
        const wrapper = document.createElement('div');
        const app = createApp({
            render: () => h(MyVueComponent, { data: itemData, index })
        });
        app.mount(wrapper);
        return wrapper;
    }
});

// React 中使用核心类
import { createRoot } from 'react-dom/client';
import VirtualList from 'virtual-list-common';
import MyReactComponent from './MyReactComponent';

const virtualList = new VirtualList(container, items, {
    onRenderItem: (index, itemData) => {
        const wrapper = document.createElement('div');
        const root = createRoot(wrapper);
        root.render(<MyReactComponent data={itemData} index={index} />);
        return wrapper;
    }
});

组件/类 API 参考

通用组件 Props

Vue 3 和 React 组件属性基本一致。以下为通用属性表格,框架特有内容见后续小节。

属性 类型 默认值 说明
items Array 必填 列表数据数组
itemHeight number 310 每个项目的高度(像素)
itemWidth number 200 每个项目的宽度(像素)
columns number 2 列数
layout 'grid' | 'masonry' | 'list' 'grid' 布局模式:'grid'(规则网格,默认)、'masonry'(瀑布流)、'list'(单列,等价于 columns: 1)
gap number | [number, number] | { row: number; column: number } 5 项目间距。支持三种格式:5(行列同距)、[10, 5](行距10/列距5)、{ row: 10, column: 5 }
buffer number 2 预渲染缓冲行数。在可视区域上下各额外保留指定行数进行渲染,避免快速滚动时出现空白区域。值越大滚动越平滑,但占用内存越多。高性能设备默认值自动降为 1
height string | number '100%' 容器高度
width string | number '100%' 容器宽度(横向模式下生效,默认填满父容器)
className string '' 容器 CSS 类名
style Object {} 容器内联样式
preloadCount number 24 初始预加载数量
preloadMaxConcurrency number 3 最大并发图片预加载请求数(v0.7.0+)
scrollAdapter ScrollAdapter | null undefined 自定义滚动适配器。undefined=原生滚动(默认),null=原生滚动,传入实例=自定义适配器
renderItem Function null 自定义渲染函数 (index, itemData) => VNode(Vue)或 ReactElement(React)或 HTMLElement(通用)
enableInfiniteScroll boolean false 是否启用无限滚动功能
loadingIndicator HTMLElement | string | null null 加载指示器。支持三种形式:HTMLElement(深拷贝)/ HTML 字符串(含 HTML 标签时解析为片段)/ 纯文本(无 HTML 标签时用作默认 spinner 模板的文案,默认 '加载中...')
endReachedMessage string '没有更多数据' 没有更多数据时显示的提示信息
infiniteScrollThreshold number 200 触发加载更多的距离阈值(像素)
infiniteScrollWaitForImages boolean false 无限滚动加载新批次数据后,是否等待该批次中所有图片加载完成才隐藏 loading 指示器(v0.7.0+)
orientation string 'vertical' 滚动方向,可选值:'vertical'(垂直)或 'horizontal'(横向)
maxRows number Infinity 横向滚动模式下的最大行数,默认值为 1(当设置为 Infinity 时)
enableAutoHeight boolean false 是否启用自适应高度(仅垂直模式有效)
estimatedItemHeight number 200 自适应高度模式下的预估项目高度(像素)
grouping object — 分组配置,详见 分组支持。包含 groups(分组数据)、headerHeight(分组头高度)、renderHeader(自定义分组头渲染)
masonry object — 瀑布流配置,详见 瀑布流布局。包含 columns(瀑布流专属列数)、gap(瀑布流列间距覆盖)。需搭配 layout="masonry" 和 enableAutoHeight: true 使用
onError Function null 错误处理回调 (error, context) => void(v0.6.0+)
onWarning Function null 警告回调 (warning, context) => void。默认静默——实现此回调可接收配置冲突等软警告:(msg) => console.warn(msg)(v0.7.0+)

事件

所有回调事件均通过 prop 统一传递,Vue 和 React 用法一致,无需区分 @event 与 onEvent:

Prop 参数 说明
onItemClick (index, itemData, element) => void 项目点击时触发
onScroll (scrollTop, firstVisibleIndex, lastVisibleIndex) => void 滚动时触发
onLoadMore () => Promise<boolean | T[] | void> 无限滚动加载更多时触发

Vue 特有

作用域插槽

Vue 组件支持通过默认作用域插槽渲染列表项,优先级高于 renderItem prop:

名称 类型 参数 说明
default Scoped Slot { index, itemData } 默认作用域插槽 #default="{ index, itemData }"

React 特有

Render Prop

React 组件支持通过 children render prop 渲染列表项,优先级高于 renderItem prop:

属性 类型 说明
children ReactNode | Function render prop 模式 children={(index, itemData) => ReactElement | HTMLElement},优先级高于 renderItem prop

通用方法(通过 ref 调用)

Vue 和 React 组件暴露的方法完全一致:

方法名 参数 说明
scrollToIndex(index, options) index: 目标索引, options: { immediate?, duration?, easing? } 滚动到指定索引
scrollToBottom(options) options: { immediate?, duration?, easing? } 滚动到末尾(垂直模式为底部,横向模式为右侧)
scrollToTop(options) options: { immediate?, duration?, easing? } 滚动到开始位置(垂直模式为顶部,横向模式为左侧)
scrollToEnd(options) options: { immediate?, duration?, easing? } scrollToBottom 的别名,用于横向模式语义更清晰
scrollToStart(options) options: { immediate?, duration?, easing? } scrollToTop 的别名,用于横向模式语义更清晰
scrollTo(scrollTop, options) scrollTop: 目标位置, options: { immediate?, duration?, easing? } 滚动到指定位置
getFirstVisibleIndex() - 获取第一个可见项目索引
getLastVisibleIndex() - 获取最后一个可见项目索引
updateItems(newItems) newItems: 新数据数组 更新数据源
destroy() - 销毁实例,清理资源
pauseScroll() - 暂停滚动(使用 Lenis 时停止动画,原生模式无操作)
resumeScroll() - 恢复滚动(使用 Lenis 时重启动画,原生模式无操作)
switchScrollAdapter(newAdapter) newAdapter: ScrollAdapter 实例 运行时切换滚动适配器,无需手动传入容器元素
scrollToGroup(groupIdx, options) groupIdx: 分组索引, options: { immediate?, duration?, easing? } 滚动到指定分组(仅分组模式可用)
getGroupCount() - 获取分组总数
setGroups(groups) groups: VirtualListGroup[] 整体替换分组数据
addGroup(group, index?) group: VirtualListGroup, index?: number 添加一个新分组(仅分组模式);默认追加到末尾,传入 index 可插入到指定位置
appendItems(newItems) newItems: 新数据数组 增量追加数据,保留滚动位置和已有 DOM(分组/非分组模式均可)

VirtualList 核心类配置

const options = {
    itemHeight: 310,        // 项目高度
    itemWidth: 200,         // 项目宽度
    columns: 2,             // 列数
    gap: 5,                 // 间距(支持多种格式:5 | [10, 5] | { row: 10, column: 5 })
    buffer: 2,              // 预渲染缓冲行数(可视区域上下各预渲染指定行数)
    preloadCount: 24,       // 预加载数量
    preloadMaxConcurrency: 3,// 最大并发图片预加载数
    maxPoolSize: 50,        // DOM 元素池动态上限(池会自动增长和收缩)
    preloadDistance: 800,   // 预加载距离
    onRenderItem: null,     // 自定义渲染函数
    onScroll: null,         // 滚动回调
    onItemClick: null,      // 点击回调
    onWarning: null,        // 警告回调(默认静默)

    // 无限滚动配置
    enableInfiniteScroll: false,    // 是否启用无限滚动
    // 加载更多回调。存在=自动启用加载,返回 false 或空数组=停止加载
    onLoadMore: null,
    loadingIndicator: null,         // 自定义加载指示器
    endReachedMessage: '没有更多数据', // 结束提示信息
    infiniteScrollThreshold: 200,   // 触发加载阈值
    infiniteScrollWaitForImages: false, // 等待图片加载完才隐藏 loading

    // 横向滚动配置
    orientation: 'vertical',        // 滚动方向:'vertical' 或 'horizontal'
    maxRows: Infinity,              // 横向模式下最大行数(Infinity 时默认为 1 行)

    // 自适应高度配置(仅垂直模式有效)
    enableAutoHeight: false,        // 是否启用自适应高度
    estimatedItemHeight: 200,       // 预估项目高度(用于初始渲染)

    // 滚动适配器配置
    scrollAdapter: null,            // 自定义滚动适配器实例。undefined/null=原生滚动,或从 virtual-list-common/lenis 传入 ScrollAdapter 实例。
};

无限滚动功能

从 v0.2.0 开始,支持无限滚动功能,可以在用户滚动到列表底部时自动加载更多数据。

Vue 3 无限滚动示例

<template>
  <VirtualList
    :items="items"
    :itemHeight="310"
    :itemWidth="200"
    :columns="2"
    height="600px"
    :enableInfiniteScroll="true"
    :onLoadMore="loadMore"
    endReachedMessage="已经到底啦~"
    ref="virtualListRef"
  />
</template>

<script setup>
import { ref } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';
const items = ref([
  'https://picsum.photos/200/300?random=1',
  'https://picsum.photos/200/300?random=2',
  'https://picsum.photos/200/300?random=3',
  'https://picsum.photos/200/300?random=4',
  'https://picsum.photos/200/300?random=5',
  'https://picsum.photos/200/300?random=6',
]);

// 加载更多数据:返回 false 或空数组自动停止后续加载
const loadMore = async () => {
  const newItems = [];
  for (let i = 0; i <= 8; i++) {
    newItems.push(`https://picsum.photos/200/300?random=${items.value.length + i + 1}`);
  }
  if (items.value.length >=40) {
    return false; // 返回 false 表示没有更多数据
  }
  virtualListRef.value?.appendItems(newItems);
  return true;
};
</script>

核心类无限滚动示例

import VirtualList from 'virtual-list-common';

let page = 1;

const virtualList = new VirtualList(container, [], {
    itemHeight: 310,
    itemWidth: 200,
    columns: 2,
    enableInfiniteScroll: true,
    infiniteScrollThreshold: 300, // 距离底部 300px 时触发加载
    endReachedMessage: '已经到底啦~',
    // 提供 onLoadMore = 启用无限滚动;返回 false 或空数组 = 停止加载
    onLoadMore: async () => {
      const newItems = [];
      for (let i = 0; i <= 8; i++) {
        newItems.push(`https://picsum.photos/200/300?random=${items.value.length + i + 1}`);
      }
      if (items.value.length >=40) {
        return false; // 返回 false 表示没有更多数据
      }
      virtualListRef.value?.appendItems(newItems);
      return true;
    }
});

React 用法:与 Vue 类似,使用 useState 管理列表数据,useRef 引用组件实例。onLoadMore 回调中通过 setItems(prev => [...prev, ...newItems]) 追加数据,返回 false 或空数组自动停止后续加载。完整示例参见 快速开始 → React 组件。

加载指示器配置(loadingIndicator)

loadingIndicator 支持三种形式,与 endReachedMessage 的"文本可独立定制"模式对齐:

形式 行为 适用场景
HTMLElement 深拷贝用户提供的元素 复用项目中已有的复杂加载组件
HTML 字符串(含 <tag> 标签) 解析为 HTML 片段 自定义动画、Logo、进度条等
纯文本(无 HTML 标签) 用作默认 spinner 模板的文案 只想改文案、保留默认旋转动画
null / undefined 使用默认 spinner + '加载中...' 模板 不想自定义
// 1. 自定义元素(深拷贝)
loadingIndicator: document.querySelector('.my-spinner'),

// 2. HTML 字符串(完全自定义)
loadingIndicator: '<div class="my-loader"><div class="dot"></div><div class="dot"></div></div>',

// 3. 纯文本(覆盖默认文案,保留 spinner 动画)
loadingIndicator: 'Loading more...',  // 或 '加载中,请稍候'

// 4. 默认(不传或传 null)
loadingIndicator: null,

判定规则:字符串中含 <tagname ...> 形式(标签名以字母开头)即视为 HTML 片段;否则视为纯文本。这意味着 '5 < 10' 这样的字符串被正确识别为纯文本(不被解析为标签)。


横向滚动功能

从 v0.3.0 开始,支持横向滚动模式,通过设置 orientation: 'horizontal' 开启。

Vue 3 横向滚动示例

<template>
  <VirtualList
    :items="items"
    :itemHeight="200"
    :itemWidth="300"
    :columns="3"
    height="400px"
    orientation="horizontal"
    :maxRows="2"
    :gap="[0, 10]"
    ref="virtualListRef"
  />
</template>

<script setup>
import { ref } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';

const items = ref([
  'https://picsum.photos/300/200?random=1',
  'https://picsum.photos/300/200?random=2',
  // ...
]);

const virtualListRef = ref(null);

</script>

核心类横向滚动示例

import VirtualList from 'virtual-list-common';

const virtualList = new VirtualList(container, items, {
    itemHeight: 200,
    itemWidth: 300,
    columns: 3,
    orientation: 'horizontal',
    maxRows: 2,          // 限制为 2 行
    gap: [0, 10]         // 行间距 0,列间距 10
});

// 使用语义化方法
virtualList.scrollToStart();  // 滚动到左侧
virtualList.scrollToEnd();    // 滚动到右侧

React 用法:与 Vue 类似。横向模式特有 props:orientation="horizontal"、maxRows={2}。使用 scrollToStart()/scrollToEnd() 替代 scrollToTop()/scrollToBottom()。完整示例参见 快速开始 → React 组件。

横向滚动配置说明

配置项 类型 默认值 说明
orientation string 'vertical' 滚动方向,设置为 'horizontal' 启用横向滚动
maxRows number Infinity 横向模式下的最大行数,默认值为 1(当设置为 Infinity 时)
width string | number '100%' 容器宽度,可指定固定宽度(如 500)或百分比(如 '80%')

鼠标滚轮支持(横向模式)

横向模式默认支持鼠标滚轮、触控板滑动和 Shift+滚轮三种输入方式,无需任何额外配置:

输入方式 行为
鼠标滚轮(垂直滚动) 自动转译为横向滚动(deltaY → scrollLeft)
触控板垂直/水平/斜向滑动 自动转译为横向滚动(deltaX + deltaY 合并到 scrollLeft)
Shift + 滚轮 横向滚动(浏览器把 deltaY 映射到 deltaX,本库自动识别)
拖动横向滚动条 仍然有效
实现机制

横向模式下容器 CSS 为 overflow-x: auto; overflow-y: hidden,浏览器原生 wheel 行为是垂直滚动(此时会冒泡到外层页面,无法驱动列表)。BaseScrollAdapter 在 onInit 阶段自动注册一个非被动 (passive: false) 的 wheel 监听器:

  • 调用 event.preventDefault() 阻止冒泡到外层页面(避免页面整体被纵向滚动)
  • 将 event.deltaX + event.deltaY 累加到 container.scrollLeft
  • 边界由浏览器自动钳位(不超过 scrollWidth - clientWidth,不低于 0)
  • 已滚动到边界(scrollLeft 为 0 或已达最大值)时不 preventDefault,允许事件冒泡给内部嵌套的可滚动元素或外层页面
  • delta 全为 0(如静止点击)时不 preventDefault,事件正常传播
  • destroy() 时自动移除监听器,引用置 null
适用范围
  • ✅ NativeScrollAdapter — 自动获得(继承基类默认行为)
  • ✅ 自定义 ScrollAdapter(继承 BaseScrollAdapter 且 onInit 调用 super.onInit())— 自动获得
  • ✅ 自定义 ScrollAdapter(继承 BaseScrollAdapter 但 onInit 不调用 super)— 需在 onInit 中手动调用 this.setupBuiltinWheelHandler(this.container)
  • ❌ LenisScrollAdapter — 不需要(Lenis 自带 wheel 处理,且其 onInit 不调用 super,不会被基类干扰)
  • ❌ 纵向模式 — 不安装此监听器,避免误拦截浏览器原生垂直滚动

间距配置(Gap)

从 v0.6.0 开始,gap 属性支持多种格式,可以分别控制行间距和列间距:

格式 示例 说明
number gap: 5 行间距 = 列间距 = 5px(向后兼容)
[rowGap, columnGap] gap: [10, 5] 行间距 10px,列间距 5px
{ row, column } gap: { row: 10, column: 5 } 行间距 10px,列间距 5px
  • rowGap:垂直滚动模式下为行间距;横向滚动模式下为纵向项目间距
  • columnGap:垂直滚动模式下为列间距;横向滚动模式下为横向项目间距

自适应高度功能

从 v0.4.0 开始,支持自适应高度模式,允许列表项目具有不同的高度。该功能仅在垂直滚动模式下有效。

Vue 3 自适应高度示例

<template>
  <VirtualList
    :items="items"
    :itemWidth="200"
    :columns="2"
    height="600px"
    :enableAutoHeight="true"
    :estimatedItemHeight="200"
    :renderItem="renderItem"
    ref="virtualListRef"
  />
</template>

<script setup>
import { ref } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';

const items = ref([
  { title: '短内容', content: '这是一段简短的内容' },
  { title: '长内容', content: '这是一段较长的内容,可能包含多行文字...' },
  // ...
]);

const virtualListRef = ref(null);

// 自定义渲染函数 - 高度由内容决定
const renderItem = (index, itemData) => {
  const div = document.createElement('div');
  div.className = 'auto-height-item';
  div.innerHTML = `
    <h3>${itemData.title}</h3>
    <p>${itemData.content}</p>
  `;
  return div;
};

</script>

<style>
.auto-height-item {
  padding: 16px;
  background: #f5f5f5;
  border-radius: 8px;
}
</style>

核心类自适应高度示例

import VirtualList from 'virtual-list-common';

const virtualList = new VirtualList(container, items, {
    itemWidth: 200,
    columns: 2,
    enableAutoHeight: true,
    estimatedItemHeight: 200,
    onRenderItem: (index, itemData) => {
        const div = document.createElement('div');
        div.className = 'auto-height-item';
        div.innerHTML = `
            <h3>${itemData.title}</h3>
            <p>${itemData.content}</p>
        `;
        return div;
    }
});

React 用法:与 Vue 类似。设置 enableAutoHeight={true} 和 estimatedItemHeight={200}。renderItem 返回 JSX 元素,高度由内容决定。完整示例参见 快速开始 → React 组件。

自适应高度配置说明

配置项 类型 默认值 说明
enableAutoHeight boolean false 是否启用自适应高度(仅垂直模式有效)
estimatedItemHeight number 200 预估项目高度(像素),用于初始渲染和滚动位置计算

响应式布局与动态尺寸

虚拟列表开箱即用地自动响应窗口缩放与容器尺寸变化,无需手动介入即可保持布局正确与滚动位置稳定。

  • 窗口 resize:自动重算可见项目数、刷新位置缓存、重渲染;使用 requestAnimationFrame 合并同帧多次变化;组件销毁时自动解绑监听器
  • 容器尺寸变化:固定高度模式由 resize 事件覆盖;自适应高度模式为每个项目挂载尺寸观察器,内容变化(如图懒加载完成)时自动更新行高;横向模式自动重算总宽度
  • 行为保证:滚动位置不丢失、可见区连续、平滑滚动适配器(如 Lenis)会同步刷新内部尺寸缓存

注意事项

  • 容器必须由 CSS 显式给出尺寸(height / width / flex 等),不能依赖内容撑开
  • 推荐用 CSS 媒体查询 / calc() / flexbox 驱动容器变化;如必须用 JS 设置 style.width / style.height,调用一次 update() 即可触发同步重算

瀑布流布局

将每个项目放入当前最短的列中,形成参差不齐的瀑布流视觉效果。

  • layout: 'masonry':启用瀑布流布局(默认 'grid' 为规则网格)
  • masonry.columns:瀑布流专属列数(默认与 columns 相同)
  • masonry.gap:瀑布流列间距覆盖(默认与 columnGap 相同)
  • 行为保证:总高度取各列最高者(非各列之和),列累积高度在 updateItems() / setGroups() 时自动重置

自定义瀑布流列数与间距:

<VirtualList
  :items="items"
  :itemHeight="150"
  :itemWidth="200"
  :columns="4"
  layout="masonry"
  :enableAutoHeight="true"
  :estimatedItemHeight="120"
  height="600px"
  :masonry="{ columns: 3, gap: 10 }"
  :gap="[10, 8]"
/>

注意事项

  • 需启用 enableAutoHeight: true 才能获得不同高度的项目实体
  • 仅垂直模式有效,横向模式下回退为常规网格布局
  • 列内项目顺序保持数据源原始顺序
  • 同列项目的垂直间距由 rowGap 控制(默认 5px),与常规网格行为一致

分组支持

将数据按分组组织,每个分组包含标题头和下属数据项,适合分类展示场景。

  • grouping.groups: VirtualListGroup<T>[]:分组数据数组(与 items 互斥,grouping.groups 优先)
  • grouping.headerHeight:分组头高度(默认 40px)
  • grouping.renderHeader:自定义分组头渲染 (group, groupIdx) => HTMLElement

使用示例

Vue 3 组件
<template>
  <VirtualList
    :grouping="groupingConfig"
    :itemHeight="60"
    :itemWidth="200"
    :columns="3"
    height="500px"
    ref="virtualListRef"
  />
</template>

<script setup>
import { ref, h } from 'vue';
import { VirtualListVue as VirtualList } from 'virtual-list-common/vue';

const groupingConfig = {
  groups: [
    {
      id: 'cat-a', 
      title: '分类 A',
      items: ['A1', 'A2', 'A3'],
    },
    {
      id: 'cat-b', 
      title: '分类 B',
      items: ['B1', 'B2', 'B3', 'B4'],
    },
  ],
  headerHeight: 40,
  renderHeader: (group, groupIdx) => {
    return h('div', { style: 'background:#f5f5f5;padding:8px;font-weight:bold;' },
      group.title
    );
  },
};

const virtualListRef = ref(null);

// 滚动到指定分组
const scrollToGroup = (idx) => {
  virtualListRef.value?.scrollToGroup(idx, { immediate: false });
};
</script>

分组数据的更新:

// 整体替换分组数据
virtualListRef.value?.setGroups([
  { id: 'g1', title: '新分类', items: ['X', 'Y', 'Z'] },
]);

// 添加一个新分组(默认追加到末尾)
virtualListRef.value?.addGroup({ id: 'g2', title: '追加分组', items: ['M', 'N'] });

// 在指定位置插入分组(index=0 插入到开头)
virtualListRef.value?.addGroup({ id: 'g0', title: '头部分组', items: ['ZERO'] }, 0);

// 增量追加数据(会保留滚动位置和已有 DOM)
virtualListRef.value?.appendItems(['X', 'Y', 'Z']);
React 组件
import { useRef } from 'react';
import { VirtualListReact as VirtualList, VirtualListReactRef } from 'virtual-list-common/react';

function App() {
  const ref = useRef(null);

  const grouping = {
    groups: [
      { id: 'cat-a', title: 'Category A', items: ['A1', 'A2', 'A3'] },
      { id: 'cat-b', title: 'Category B', items: ['B1', 'B2', 'B3'] },
    ],
    headerHeight: 40,
    renderHeader: (group, groupIdx) => {
      const div = document.createElement('div');
      div.style.cssText = 'background:#f5f5f5;padding:8px;font-weight:bold;';
      div.textContent = group.title;
      return div;
    },
  };

  return (
    <VirtualList
      ref={ref}
      grouping={grouping}
      itemHeight={60}
      itemWidth={200}
      columns={3}
      height="500px"
    />
  );
}

注意事项

  • scrollToGroup(groupIdx, options?) — 滚动到指定分组;getGroupCount() — 获取分组总数
  • setGroups(groups) — 整体替换分组数据;addGroup(group, index?) — 添加一个新分组;appendItems(items) — 增量追加数据(分组/非分组通用,保留滚动位置);updateItems() — 会退出分组模式,切换到非分组列表
  • 无限滚动时新增数据通过 appendItems() 在最后一个分类增量追加,保留滚动位置
  • 分组头不参与元素池回收,每个分组固定 1 个标题头

配置冲突与优先级

多个配置之间可能存在重叠或互斥关系,下表汇总所有冲突的处理规则。

冲突组合 行为 说明
grouping.groups + items grouping.groups 优先 同时设置时输出 console.warn 警告;items 被忽略
masonry.columns + columns masonry.columns 优先 未设置 masonry.columns 时回退到 columns,再回退到 2
masonry.gap + columnGap masonry.gap 优先 回退链:columnGap → gap → 5
layout: 'masonry' + enableAutoHeight: false 瀑布流静默失效 瀑布流依赖自适应高度测量;未启用时回退到默认布局,layout 配置保留但不使用
layout: 'masonry' + orientation: 'horizontal' 瀑布流不生效 瀑布流仅支持垂直模式;横向滚动优先
layout: 'list' 生效,强制设为单列布局 columns 被覆盖为 1;可直接设置 columns: 1 获得相同效果
enableAutoHeight: true + itemHeight itemHeight 变为预估高度 实际高度由 ResizeObserver 动态测量;itemHeight 仅作初始占位与未测量行的回退值
updateItems(newItems) + groups 退出分组模式,切换到非分组列表 updateItems 会全量替换并销毁已有 DOM;增量追加请用 appendItems(newItems)

行为保证

  • 不会抛错:所有冲突都有兜底行为,应用继续运行
  • 静默 vs 警告:仅 groups + items 组合触发 console.warn;其他冲突均为静默回退
  • 运行时校验:构造时执行 _validateOptions() 校验参数合法性(负数、必填缺失等),但不检测行为冲突

TypeScript 支持

从 v0.4.0 开始提供完整的 TypeScript 类型支持,包括自动生成的类型声明文件。v0.5.0 起所有类型支持泛型参数(默认 string 以保持向后兼容)。

导出的类型

import VirtualList, {
  Orientation,
  EasingFunction,
  ScrollOptions,
  VirtualListOptions,
  RenderItemFunction,
  LoadMoreFunction,
  ItemClickCallback,
  GapValue,
  NativeScrollAdapter,
} from 'virtual-list-common';
// LenisScrollAdapter 需从独立入口导入(减少主包体积)
import { LenisScrollAdapter } from 'virtual-list-common/lenis';
import type {
  ScrollAdapter,
  ScrollAdapterConfig,
  ScrollToOptions,
  LenisScrollAdapterConfig,
  ParsedGap,
} from 'virtual-list-common';

import {
  VirtualListVue,
  VirtualListVueProps,
  VirtualListVueExpose
} from 'virtual-list-common/vue';

import {
  VirtualListReact,
  VirtualListReactProps,
  VirtualListReactRef
} from 'virtual-list-common/react';

类型定义说明

类型 说明
Orientation 滚动方向:'vertical' | 'horizontal'
EasingFunction 缓动函数:(t: number) => number
ScrollOptions 滚动选项:{ immediate?, duration?, easing? }
VirtualListOptions<T> 虚拟列表配置选项接口,支持泛型
RenderItemFunction<T> 自定义渲染函数类型,支持泛型
LoadMoreFunction<T> 加载更多回调函数类型,支持泛型
ItemClickCallback<T> 项目点击回调函数类型,支持泛型
VirtualListVueProps<T> Vue 组件 Props 类型,支持泛型
VirtualListVueExpose<T> Vue 组件暴露方法类型,支持泛型
VirtualListReactProps<T> React 组件 Props 类型,支持泛型
VirtualListReactRef<T> React 组件 Ref 类型,支持泛型
GapValue 间距值类型:number | [number, number] | { row: number; column: number }
ParsedGap 解析后的间距:{ rowGap: number; columnGap: number }

滚动适配器模式(v0.6.0+)

从 v0.6.0 开始,虚拟列表采用适配器模式管理滚动,Lenis不再是核心依赖,支持自由选择滚动实现。

内置适配器

适配器 依赖 说明
NativeScrollAdapter 零依赖 浏览器原生滚动,默认适配器。支持垂直/水平方向。
LenisScrollAdapter lenis(可选) 包装 Lenis 库,提供平滑滚动体验。支持动态 import 或显式传入 Lenis 类。

默认行为

  • 不传任何滚动配置:使用 NativeScrollAdapter,零依赖原生滚动
  • scrollAdapter: null:强制使用 NativeScrollAdapter(原生滚动)
  • 传入 scrollAdapter 实例:直接使用自定义适配器(如 new LenisScrollAdapter(...),需从 virtual-list-common/lenis 导入)

使用 Lenis 平滑滚动

// 显式传入 LenisScrollAdapter(唯一推荐方式)
import { LenisScrollAdapter } from 'virtual-list-common/lenis';
import Lenis from 'lenis';

const adapter = new LenisScrollAdapter({
    orientation: 'vertical',
    onScroll: () => {},
    lenis: Lenis,             // 传入 Lenis 类避免动态 import
    lerp: 0.1,                // 滚动阻尼
    smoothWheel: true,
    wheelMultiplier: 1,
});

const list = new VirtualList(container, items, {
    scrollAdapter: adapter,
});

Vue/React 组件使用:直接传入 scrollAdapter prop 即可,无需额外配置:

<!-- Vue -->
<VirtualList :items="data" :scrollAdapter="adapter" />
/* React */
<VirtualList items={data} scrollAdapter={adapter} />

编写自定义适配器

推荐方式:继承 BaseScrollAdapter 抽象基类。基类已提供 init 模板、orientation 覆盖、容器引用管理、getScrollPos DOM 兜底、setOnScroll 链式、destroy 通用清理、resize 默认 no-op 等公共行为,子类仅需实现 scrollTo 即可。

import { BaseScrollAdapter } from 'virtual-list-common';
import type { ScrollToOptions } from 'virtual-list-common';
import SmoothScrollbar from 'smooth-scrollbar';

class MyScrollbarAdapter extends BaseScrollAdapter {
    private sb: any = null;

    // 子类特定的初始化钩子(可选重写,默认 addEventListener)
    protected override onInit(container: HTMLElement, _contentContainer: HTMLElement): void {
        this.sb = SmoothScrollbar.init(container);
        this.sb.addListener(() => this.config.onScroll());
    }

    // 子类特定的清理钩子(可选重写,默认 removeEventListener)
    protected override onDestroy(): void {
        super.onDestroy(); // 调用基类默认 removeEventListener(虽然本例未使用)
        this.sb?.destroy();
        this.sb = null;
    }

    // 必须实现:getScrollPos 默认从 DOM 读取,若滚动库有内部状态可重写
    override getScrollPos(): number {
        return this.isHorizontal
            ? (this.sb?.offset?.x ?? 0)
            : (this.sb?.offset?.y ?? 0);
    }

    // 必须实现:scrollTo 是各适配器差异最大的部分
    override scrollTo(targetPos: number, options?: ScrollToOptions): void {
        // duration 单位是秒,SmoothScrollbar 用毫秒
        const durationMs = options?.duration ? options.duration * 1000 : 0;
        if (this.isHorizontal) {
            this.sb?.scrollTo(targetPos, 0, durationMs);
        } else {
            this.sb?.scrollTo(0, targetPos, durationMs);
        }
    }

    // Lenis 等需要的手动控制(可选)
    override pause(): void {
        this.sb?.pause();
    }

    override resume(): void {
        this.sb?.resume();
    }

    // 内容尺寸变化时通知底层(必选接口方法;基类默认 no-op)
    override resize(): void {
        this.sb?.update();
    }
}

// 使用
const list = new VirtualList(container, items, {
    scrollAdapter: new MyScrollbarAdapter({ orientation: 'vertical', onScroll: () => {} }),
});

备选方式:直接 implements ScrollAdapter。需要自己实现所有必选方法(init / getScrollPos / scrollTo / setOnScroll / destroy / resize),适合需要完全控制内部状态的高级场景。一般不推荐。

import type { ScrollAdapter, ScrollAdapterConfig, ScrollToOptions } from 'virtual-list-common';
import SmoothScrollbar from 'smooth-scrollbar';

class MyScrollbarAdapter implements ScrollAdapter {
    // ... 需自行实现所有 6 个必选方法
}

运行时切换适配器

// 从 Lenis 切换到原生
const nativeAdapter = new NativeScrollAdapter({ orientation: 'vertical', onScroll: () => {} });
await virtualList.switchScrollAdapter(nativeAdapter);

switchScrollAdapter 是 VirtualList 的公开方法,内部自动传入容器元素,无需自行获取 contentContainer。

注意:实例 destroy() 后调用 switchScrollAdapter 无效——控制台会输出 [virtual-list] switchScrollAdapter 在实例销毁后调用,已忽略 警告并被忽略。

适配器相关导出

// 从主入口导出
import {
    BaseScrollAdapter,      // 抽象基类(推荐自定义适配器继承)
    NativeScrollAdapter,
} from 'virtual-list-common';
import type {
    ScrollAdapter,
    ScrollAdapterConfig,
    ScrollToOptions,
} from 'virtual-list-common';
// LenisScrollAdapter 需从独立入口导入(减少主包体积)
import { LenisScrollAdapter } from 'virtual-list-common/lenis';
import type { LenisScrollAdapterConfig } from 'virtual-list-common/lenis';

注意事项

  1. 生命周期管理:Vue/React 组件销毁时自动清理资源。使用核心类需手动调用 virtualList.destroy()。

  2. 响应式数据:Vue 组件 items 是响应式的,数据变化自动更新列表;React 组件 items 变化自动触发更新;核心类需调用 virtualList.updateItems(newItems)。

  3. 依赖:

    • 核心库零必选依赖,默认使用浏览器原生滚动
    • 如需平滑滚动,可选安装 lenis 并通过 LenisScrollAdapter 使用
    • Vue 组件需要 Vue 3.x,React 组件需要 React 16.8+(支持 Hooks)
  4. CSS 定位:项目元素使用 position: absolute,行内 margin 不会生效 — 请使用 gap 属性控制间距;容器需要显式高度,不建议额外嵌套滚动元素

  5. 性能与最佳实践:

    • 内部对 < 3px 的滚动位移跳过渲染更新,降低高频抖动开销(v0.7.0 优化)
    • 使用 Map 存储活跃元素,避免稀疏数组内存泄漏(v0.5.0)
    • 保持默认 buffer: 2 和 preloadCount: 24 可获得性能与内存的最佳平衡。buffer 控制可视区域外预渲染的行数 — 增大可减少滚动空白,但会增加 DOM 节点和内存开销
    • 超大数据集(10,000+ 项)建议启用 enableInfiniteScroll 实现按需加载
    • 渲染函数应尽量轻量化,避免复杂的 DOM 操作
  6. 滚动控制:

    • pauseScroll() / resumeScroll() 适用于弹窗、抽屉等场景
    • 原生滚动模式下这两个方法为无操作(安全调用)
    const openModal = () => {
      virtualListRef.value?.pauseScroll();
      showModal();
    };
    const closeModal = () => {
      hideModal();
      virtualListRef.value?.resumeScroll();
    };
    

图片懒加载

虚拟列表内置图片懒加载与预加载系统,基于 IntersectionObserver,开箱即用。通过 preloadDistance、preloadCount 和 preloadMaxConcurrency 选项调整预加载行为(见 API 参考)。

无限滚动 + 图片等待:启用 infiniteScrollWaitForImages: true 后,无限滚动加载新批次数据时会保持 loading 指示器可见,直到该批次中所有图片加载完成,避免图片未加载就允许继续滚动触发下一次加载(v0.7.0+)。


CSS 自定义

虚拟列表生成以下 DOM 结构,通过覆盖这些 class 的样式即可自定义外观:

<div class="virtual-list-container" style="height:600px;overflow:auto;">
  <div class="virtual-list-content" style="position:relative;width:...;height:...;">
    <div class="virtual-list-item" style="position:absolute;top:...;left:...;width:...;height:...;">
      <!-- 你的渲染内容 -->
    </div>
    <div class="virtual-list-loading-indicator">加载中...</div>
    <div class="virtual-list-end-message">没有更多数据</div>
  </div>
</div>

列表项使用 position: absolute 定位,margin 不会生效,请使用 gap 属性控制间距。


错误处理

核心类和 Vue / React 组件均支持 onError 回调,用于在生产环境中捕获内部异常。

import VirtualList from 'virtual-list-common';

const virtualList = new VirtualList(container, items, {
    onError: (error, context) => {
        // context 示例: 'scroll-adapter-init', 'image-preload', 'infinite-scroll'
        console.warn(`[VirtualList] 错误 (${context}):`, error);
        // 接入自己的错误上报
        reportError({ message: error.message, context });
    }
});
<!-- Vue 组件 -->
<VirtualList :items="items" :onError="handleError" ... />
{/* React 组件 */}
<VirtualList items={items} onError={handleError} ... />

浏览器兼容性

API Chrome Firefox Safari Edge 备注
核心虚拟列表 91+ 90+ 15+ 91+ 基于 ES2022 构建(#private fields、Error.cause 等)
IntersectionObserver(图片懒加载) 51+ 55+ 12.1+ 15+ 不支持时图片直接加载,无懒加载
ResizeObserver(自适应高度) 64+ 69+ 13.1+ 79+ 不支持时自适应高度不可用
scroll-behavior: smooth 61+ 36+ 15.4+ 79+ 平滑滚动降级时使用 JS 动画

常见问题

Q: 项目元素设置了 margin 但没有间距效果?

A: 列表项使用 position: absolute 定位,margin 不会生效。请使用 gap 属性控制间距:

new VirtualList(container, items, {
    gap: 10,                   // 行距 = 列距 = 10px
    // 或
    gap: [16, 8],              // 行距 16px, 列距 8px
    // 或
    gap: { row: 16, column: 8 }
});
Q: 更新数据后列表没有刷新?

A: 使用核心类时需手动调用:

virtualList.updateItems(newItems);

使用 Vue / React 组件时,只需更新 items 响应式数据即可。

Q: 横向滚动如何启用平滑滚动?

A: 使用 LenisScrollAdapter 时设置 orientation: 'horizontal':

const adapter = new LenisScrollAdapter({
    orientation: 'horizontal',
    onScroll: () => {},
    lenis: Lenis,
});
const list = new VirtualList(container, items, {
    orientation: 'horizontal',
    scrollAdapter: adapter,
});
Q: 横向模式下鼠标滚轮不能滚动列表,只能拖动滚动条?

A: 该问题已在 v0.7.0+ 修复。BaseScrollAdapter 在横向模式下默认注册一个 wheel 事件监听器,把 deltaY / deltaX 累加到 scrollLeft:

  • 鼠标滚轮(deltaY)→ 横向滚动
  • 触控板垂直滑动(deltaY)→ 横向滚动
  • 触控板水平滑动(deltaX)→ 横向滚动
  • 触控板斜向滑动 → 合并为横向滚动
  • Shift + 滚轮 → 横向滚动(浏览器把 deltaY 映射到 deltaX)

⚠️ 自定义适配器注意:如果你继承 BaseScrollAdapter 并重写 onInit 没有调用 super.onInit(container, contentContainer),基类的 wheel 转译器不会被安装,需要在 onInit 中手动调用 this.setupBuiltinWheelHandler(this.container) 才能获得此行为。

Q: 自定义 ScrollAdapter 在横向模式下仍然是纵向滚动?

A: 该问题已在 v0.7.0+ 修复。横向模式默认会安装 wheel → scrollLeft 转译器,阻止浏览器把 wheel 事件冒泡到外层页面触发页面级纵向滚动。

如果你之前在自定义 ScrollAdapter 里手动写过 wheel 监听器(之前因为库不处理,临时方案),现在可以删除那段代码,基类会自动接管。如果你的自定义适配器重写了 onInit 但没有调用 super.onInit(),请参考上一个 FAQ 的注意事项手动调用 setupBuiltinWheelHandler。

Q: columns / maxRows / masonry.columns 传 0 或负数会怎样?

A: 会被静默纠正为 1,不会产生 NaN 布局或崩溃。该保护对 columns、maxRows(横向模式)和 masonry.columns 均生效。

Q: 在 SSR / Nuxt / Next.js 中使用报错?

A: 虚拟列表依赖浏览器 DOM API。在 SSR 框架中需确保仅客户端渲染:

// Nuxt 3
<ClientOnly>
  <VirtualList ... />
</ClientOnly>

// Next.js (App Router)
'use client';
import { VirtualListReact } from 'virtual-list-common/react';

v0.6.0 → v0.7.0

方面 v0.6.0 v0.7.0
scrollDamping / smoothScroll / itemSpacing 可用但标记废弃 正式移除——这些属性不再有任何效果,请使用 scrollAdapter + gap 替代
分组/瀑布流 扁平配置(stickyRows / columns / groups 平铺在顶层) 嵌套配置 — 归入 masonry / grouping 子对象(sticky 已移除)
运行时切换滚动 不支持 新增 switchScrollAdapter(adapter) 公共方法
默认滚动 浏览器原生滚动(零依赖) 不变
需要关注的情况
  1. 使用 scrollDamping 的 → 改为 scrollAdapter: new LenisScrollAdapter({ lerp: 0.09 })
  2. 使用 smoothScroll: true 的 → 改为手动导入 LenisScrollAdapter
  3. 使用 itemSpacing 的 → 改为 gap 属性(支持 number / [row, col] / { row, col })
  4. 已使用 scrollAdapter / gap 的 → 无需改动

Related Packages

@pdanpdan/virtual-scroll

Virtual scrolling for Vue 3: only the rows near the viewport are in the DOM.

8,720 13
@ceriousdevtech/cerious-scroll

High-performance virtual scrolling for variable-height lists, native tables, and...

4,865 44
@lazy-virtual-scroll/vue

A high-performance virtualized list component for Vue 3 that efficiently renders...

2,936 0
vue-virtual-scroller-kit

Virtual list, table, grid and tree for Vue 3 — dynamic row heights, grouping, st...

3,195 0
sweet-album

A framework-agnostic web photo album: justified layout, virtual scrolling, year/...

1,025 2

Version History

Version Released Vue License
0.7.1 >=3.0.0 ISC
0.7.0 >=3.0.0 ISC
0.6.4 >=3.0.0 ISC
0.6.3 >=3.0.0 ISC
0.6.2 >=3.0.0 ISC