virtual-list-common
v0.7.0
Published
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.
Maintainers
Readme
virtual-list-common 虚拟列表
Vue 3 / React 通用虚拟列表,高效渲染海量数据。仅渲染可见区域的 DOM 并借助元素池复用,确保大数据场景下丝滑流畅。
目录
- 特性
- 安装
- 快速开始
- API 参考
- 无限滚动
- 横向滚动
- 自适应高度
- 响应式布局与动态尺寸
- 瀑布流布局
- 分组支持
- 配置冲突与优先级
- TypeScript 支持
- 滚动适配器模式
- 图片懒加载
- CSS 自定义
- 错误处理
- 注意事项
- 浏览器兼容性
- 常见问题
- 迁移指南
特性
- 框架无关核心 + 原生组件 — 核心类零框架依赖;同时提供 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 = `<img src="${itemData}" alt="Item ${index}" /><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">
<img :src="itemData" :alt="`Item ${index}`" />
<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 和
renderItemprop 时,插槽/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) 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才能获得不同高度的项目实体 - 仅垂直模式有效,横向模式下回退为常规网格布局
- 列内项目顺序保持数据源原始顺序
分组支持
将数据按分组组织,每个分组包含标题头和下属数据项,适合分类展示场景。
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 组件使用:直接传入
scrollAdapterprop 即可,无需额外配置:<!-- 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。
适配器相关导出
// 从主入口导出
import {
BaseScrollAdapter, // 抽象基类(推荐自定义适配器继承)
NativeScrollAdapter,
LenisScrollAdapter,
} from 'virtual-list-common';
import type {
ScrollAdapter,
ScrollAdapterConfig,
ScrollToOptions,
LenisScrollAdapterConfig,
} from 'virtual-list-common';注意事项
生命周期管理:Vue/React 组件销毁时自动清理资源。使用核心类需手动调用
virtualList.destroy()。响应式数据:Vue 组件
items是响应式的,数据变化自动更新列表;React 组件items变化自动触发更新;核心类需调用virtualList.updateItems(newItems)。依赖:
- 核心库零必选依赖,默认使用浏览器原生滚动
- 如需平滑滚动,可选安装
lenis并通过LenisScrollAdapter使用 - Vue 组件需要 Vue 3.x,React 组件需要 React 16.8+(支持 Hooks)
CSS 定位:项目元素使用
position: absolute,行内margin不会生效 — 请使用gap属性控制间距;容器需要显式高度,不建议额外嵌套滚动元素性能与最佳实践:
- 内部对 < 3px 的滚动位移跳过渲染更新,降低高频抖动开销(v0.7.0 优化)
- 使用
Map存储活跃元素,避免稀疏数组内存泄漏(v0.5.0) - 保持默认
buffer: 2和preloadCount: 24可获得性能与内存的最佳平衡。buffer控制可视区域外预渲染的行数 — 增大可减少滚动空白,但会增加 DOM 节点和内存开销 - 超大数据集(10,000+ 项)建议启用
enableInfiniteScroll实现按需加载 - 渲染函数应尽量轻量化,避免复杂的 DOM 操作
滚动控制:
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 | 备注 |
|-----|--------|---------|--------|------|------|
| 核心虚拟列表 | 49+ | 52+ | 10+ | 79+ | 依赖 requestAnimationFrame、Map、Set |
| IntersectionObserver(图片懒加载) | 51+ | 55+ | 12.1+ | 15+ | 不支持时图片直接加载,无懒加载 |
| ResizeObserver(自适应高度) | 64+ | 69+ | 13.1+ | 79+ | 不支持时自适应高度不可用 |
| scroll-behavior: smooth | 61+ | 36+ | 15.4+ | 79+ | 平滑滚动降级时使用 JS 动画 |
常见问题
A: 列表项使用 position: absolute 定位,margin 不会生效。请使用 gap 属性控制间距:
new VirtualList(container, items, {
gap: 10, // 行距 = 列距 = 10px
// 或
gap: [16, 8], // 行距 16px, 列距 8px
// 或
gap: { row: 16, column: 8 }
});A: 使用核心类时需手动调用:
virtualList.updateItems(newItems);使用 Vue / React 组件时,只需更新 items 响应式数据即可。
A: 使用 LenisScrollAdapter 时设置 orientation: 'horizontal':
const adapter = new LenisScrollAdapter({
orientation: 'horizontal',
onScroll: () => {},
lenis: Lenis,
});
const list = new VirtualList(container, items, {
orientation: 'horizontal',
scrollAdapter: adapter,
});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)才能获得此行为。
A: 该问题已在 v0.7.0+ 修复。横向模式默认会安装 wheel → scrollLeft 转译器,阻止浏览器把 wheel 事件冒泡到外层页面触发页面级纵向滚动。
如果你之前在自定义 ScrollAdapter 里手动写过 wheel 监听器(之前因为库不处理,临时方案),现在可以删除那段代码,基类会自动接管。如果你的自定义适配器重写了 onInit 但没有调用 super.onInit(),请参考上一个 FAQ 的注意事项手动调用 setupBuiltinWheelHandler。
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) 公共方法 |
| 默认滚动 | 浏览器原生滚动(零依赖) | 不变 |
需要关注的情况
- 使用
scrollDamping的 → 改为scrollAdapter: new LenisScrollAdapter({ lerp: 0.09 }) - 使用
smoothScroll: true的 → 改为手动导入LenisScrollAdapter - 使用
itemSpacing的 → 改为gap属性(支持number/[row, col]/{ row, col }) - 已使用
scrollAdapter/gap的 → 无需改动
