npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

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 = `<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 和 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 用法一致,无需区分 @eventonEvent

| 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+滚轮三种输入方式,无需任何额外配置:

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

实现机制

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

  • 调用 event.preventDefault() 阻止冒泡到外层页面(避免页面整体被纵向滚动)
  • event.deltaX + event.deltaY 累加到 container.scrollLeft
  • 边界由浏览器自动钳位(不超过 scrollWidth - clientWidth,不低于 0)
  • delta 全为 0(如静止点击)时 preventDefault,事件正常传播
  • destroy() 时自动移除监听器,引用置 null

适用范围

  • NativeScrollAdapter — 自动获得(继承基类默认行为)
  • ✅ 自定义 ScrollAdapter(继承 BaseScrollAdapteronInit 调用 super.onInit())— 自动获得
  • ✅ 自定义 ScrollAdapter(继承 BaseScrollAdapteronInit 不调用 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 优先 | 回退链:columnGapgap5 | | 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

适配器相关导出

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

注意事项

  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: 2preloadCount: 24 可获得性能与内存的最佳平衡。buffer 控制可视区域外预渲染的行数 — 增大可减少滚动空白,但会增加 DOM 节点和内存开销
    • 超大数据集(10,000+ 项)建议启用 enableInfiniteScroll 实现按需加载
    • 渲染函数应尽量轻量化,避免复杂的 DOM 操作
  6. 滚动控制

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

图片懒加载

虚拟列表内置图片懒加载与预加载系统,基于 IntersectionObserver,开箱即用。通过 preloadDistancepreloadCountpreloadMaxConcurrency 选项调整预加载行为(见 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+ | 依赖 requestAnimationFrameMapSet | | 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) 公共方法 | | 默认滚动 | 浏览器原生滚动(零依赖) | 不变 |

需要关注的情况

  1. 使用 scrollDamping → 改为 scrollAdapter: new LenisScrollAdapter({ lerp: 0.09 })
  2. 使用 smoothScroll: true → 改为手动导入 LenisScrollAdapter
  3. 使用 itemSpacing → 改为 gap 属性(支持 number / [row, col] / { row, col }
  4. 已使用 scrollAdapter / gap 的 → 无需改动