TanStack Virtual 的 Lit 适配器实战:固定尺寸行/列/网格虚拟化示例深度解析
发布时间:2026/9/29 5:59:44 作者:尧图编辑部 阅读量:1,286

前端UI组件【免费下载链接】virtual Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte项目地址https://gitcode.com/gh_mirrors/vi/virtual点击查看免费下载本文基于本仓库中的 Lit 固定尺寸fixed示例讲解如何使用tanstack/lit-virtual的VirtualizerController在 Web ComponentsLit中实现行、列与网格三种固定尺寸虚拟化列表。读完本文你将掌握示例的运行方式、三种虚拟化组件的完整实现、VirtualizerController的底层响应式机制以及tanstack/virtual-core中与固定尺寸场景相关的核心选项与默认值。示例概况与运行方式本示例位于 examples/lit/fixed其官方说明README.md给出的运行步骤非常简短安装依赖npm install启动开发服务器npm run start不过对照该目录下的 package.json 可以发现实际定义的脚本为devvite、buildtsc vite build与servevite preview并未定义start脚本。因此在实际运行时请以 package.json 为准npm install npm run dev开发服务器启动后浏览器加载 index.htmlbody div idroot/div script typemodule src/src/main.ts/script my-app/my-app /body页面通过script typemodule引入 src/main.ts然后挂载一个名为my-app的自定义元素。该元素内部依次渲染三个自定义元素row-virtualizer-fixed行、column-virtualizer-fixed列和grid-virtualizer-fixed网格并在顶部给出说明文字——这些组件使用固定尺寸即每个元素的尺寸被硬编码为同一个常量且永不变化。示例的依赖与版本见 package.jsontanstack/lit-virtual^3.14.2Lit 适配器提供VirtualizerController/WindowVirtualizerControllertanstack/virtual-core^3.17.11框架无关的核心虚拟化引擎lit^3.3.0组件基类与响应式控制器faker-js/faker^8.4.1生成测试数据vite^6.4.2与typescript5.9.3构建工具链固定尺寸行虚拟化RowVirtualizerFixed行虚拟化是最典型的垂直列表场景。核心实现如下节选自 src/main.tscustomElement(row-virtualizer-fixed) class RowVirtualizerFixed extends LitElement { private scrollElementRef: RefHTMLDivElement createRef() private virtualizerController: VirtualizerControllerHTMLDivElement, Element constructor() { super() this.virtualizerController new VirtualizerController(this, { getScrollElement: () this.scrollElementRef.value, count: 10000, estimateSize: () 35, overscan: 5, }) } render() { const virtualizer this.virtualizerController.getVirtualizer() const virtualRows virtualizer.getVirtualItems() return html div div classlist scroll-container ${ref(this.scrollElementRef)} div styleposition: relative; height: ${virtualizer.getTotalSize()}px; width: 100%; ${repeat( virtualRows, (virtualRow) virtualRow.key, (virtualRow) html div class${virtualRow.index % 2 0 ? list-item-even : list-item-odd} styleposition: absolute; left: 0; top: 0; width: 100%; height: ${virtualRow.size}px; transform: translateY(${virtualRow.start}px) Row ${virtualRow.index} /div, )} /div /div /div ... } }该组件的关键设计可拆解为四层结构滚动容器一个带overflow: auto的普通div通过 Lit 的ref指令ref(this.scrollElementRef)把 DOM 引用交给getScrollElement。在本例中滚动容器高度为 200px见.scroll-container样式。撑高容器sizer内层的空div使用position: relative其高度设置为virtualizer.getTotalSize()。getTotalSize()返回所有虚拟化项的总像素高度它决定了滚动条的实际长度。垂直列表算高度height: ${totalSize}px水平列表则算宽度。虚拟项渲染通过repeat指令按virtualRow.key渲染当前可见项。每一项使用position: absolutetranslateY(${virtualRow.start}px)定位高度固定为virtualRow.size。虚拟器控制器new VirtualizerController(this, {...})将虚拟器绑定到 Lit 组件的生命周期上getVirtualizer()在render()中取出Virtualizer实例getVirtualItems()返回当前需要渲染的项数组。这里体现了固定尺寸场景的核心思路每一项都拥有确定的size与start起始位置因此可以用纯 CSS 变换精确摆放无需在渲染后测量真实 DOM 尺寸。固定尺寸列虚拟化ColumnVirtualizerFixed列虚拟化与行虚拟化几乎对称区别仅在于开启horizontal: truecustomElement(column-virtualizer-fixed) class ColumnVirtualizerFixed extends LitElement { private scrollElementRef: RefHTMLDivElement createRef() private virtualizerController: VirtualizerControllerHTMLDivElement, Element constructor() { super() this.virtualizerController new VirtualizerController(this, { getScrollElement: () this.scrollElementRef.value, count: sentences.length, estimateSize: () 100, horizontal: true, }) } render() { const virtualizer this.virtualizerController.getVirtualizer() const virtualColumns virtualizer.getVirtualItems() return html div div classlist scroll-container ${ref(this.scrollElementRef)} div styleposition: relative; height: 100%; width: ${virtualizer.getTotalSize()}px; ${repeat( virtualColumns, (virtualColumn) virtualColumn.key, (virtualColumn) html div class${virtualColumn.index % 2 0 ? list-item-even : list-item-odd} styleposition: absolute; left: 0; top: 0; height: 100%; width: ${virtualColumn.size}px; transform: translateX(${virtualColumn.start}px) Column ${virtualColumn.index} /div, )} /div /div /div ... } }与行版本的三处对照维度行虚拟化列虚拟化选项horizontal缺省falsehorizontal: true撑高容器height: ${totalSize}px宽度 100%width: ${totalSize}px高度 100%项定位translateY(${start}px)translateX(${start}px)项尺寸高度 virtualRow.size宽度 virtualColumn.size滚动轴scrollTop垂直scrollLeft水平数据方面列的数量取自sentences.length即预先生成的 10000 条 faker 句子每条句子 2070 个单词faker.lorem.sentence(randomNumber(20, 70))但每列宽度仍固定为estimateSize: () 100。这正体现了“固定尺寸”的含义——内容数据可以各不相同但虚拟化层使用的尺寸始终是常量。固定尺寸网格虚拟化GridVirtualizerFixed网格示例演示了一个强大的特性同一个滚动容器上同时挂载两个独立控制器一个负责行、一个负责列customElement(grid-virtualizer-fixed) class GridVirtualizerFixed extends LitElement { private scrollElementRef: RefHTMLDivElement createRef() private rowVirtualizerController: VirtualizerControllerHTMLDivElement, Element private columnVirtualizerController: VirtualizerControllerHTMLDivElement, Element constructor() { super() this.rowVirtualizerController new VirtualizerController(this, { getScrollElement: () this.scrollElementRef.value, count: sentences.length, estimateSize: () 35, overscan: 5, }) this.columnVirtualizerController new VirtualizerController(this, { getScrollElement: () this.scrollElementRef.value, count: sentences.length, estimateSize: () 100, horizontal: true, overscan: 5, }) } render() { const rowVirtualizer this.rowVirtualizerController.getVirtualizer() const columnVirtualizer this.columnVirtualizerController.getVirtualizer() return html div div classlist scroll-container ${ref(this.scrollElementRef)} div styleposition: relative; height: ${rowVirtualizer.getTotalSize()}px; width: ${columnVirtualizer.getTotalSize()}px; ${repeat( rowVirtualizer.getVirtualItems(), (virtualRow) virtualRow.key, (virtualRow) repeat( columnVirtualizer.getVirtualItems(), (virtualColumn) virtualColumn.key, (virtualColumn) html div class... styleposition: absolute;left: 0; top: 0; width: ${virtualColumn.size}px; height: ${virtualRow.size}px; transform: translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px) Cell ${virtualRow.index}, ${virtualColumn.index} /div , ), )} /div /div /div ... } }网格的撑高容器需要同时写入height与width高度由行控制器决定rowVirtualizer.getTotalSize()宽度由列控制器决定columnVirtualizer.getTotalSize()。单元格用repeat嵌套外层遍历可见行内层遍历可见列每个单元格的宽高分别取virtualColumn.size与virtualRow.size定位则同时使用translateX与translateY。这种“双控制器”模式是 TanStack Virtual 实现网格虚拟化的官方范式——网格并不是一个独立的虚拟器类型而是由两个正交的一维虚拟器组合而成二者共享同一个滚动元素。VirtualizerControllerLit 响应式控制器适配层固定尺寸示例中反复出现的VirtualizerController定义在 packages/lit-virtual/src/index.ts它是本仓库为 Lit 提供的一层薄封装官方文档见 docs/framework/lit/lit-virtual.md。其核心实现如下class VirtualizerControllerBase TScrollElement extends Element | Window, TItemElement extends Element, implements ReactiveController { host: ReactiveControllerHost private readonly virtualizer: VirtualizerTScrollElement, TItemElement private cleanup: () void () {} constructor( host: ReactiveControllerHost, options: VirtualizerOptionsTScrollElement, TItemElement, ) { const resolvedOptions { ...options, onChange: (instance, sync) { this.host.updateComplete.then(() this.host.requestUpdate()) options.onChange?.(instance, sync) }, } this.virtualizer new Virtualizer(resolvedOptions) ;(this.host host).addController(this) } public getVirtualizer() { return this.virtualizer } hostConnected() { this.cleanup this.virtualizer._didMount() } hostUpdated() { this.virtualizer._willUpdate() } hostDisconnected() { this.cleanup() } }这段代码揭示了适配器与 Lit 生命周期钩子的映射关系从源码结构看hostConnected()→virtualizer._didMount()当 Lit 元素被连接到文档时虚拟器开始安装内部会建立滚动监听与ResizeObserver返回值是一个用于卸载的清理函数。hostUpdated()→virtualizer._willUpdate()每次组件更新后虚拟器同步读取最新的scrollElement、scrollRect 与 scrollOffset并触发可见范围重算。hostDisconnected()→cleanup()元素被移除时取消全部订阅避免内存泄漏。onChange桥接虚拟器内部状态变化时先等待host.updateComplete再调用host.requestUpdate()把虚拟化计算的结果安全地送进 Lit 的渲染批次避免在渲染中途写入 DOM。在此基础上VirtualizerController元素滚动与WindowVirtualizerController窗口滚动分别注入了不同的底层实现export class VirtualizerController... extends VirtualizerControllerBase... { constructor(host, options) { super(host, { observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, ...options, }) } } export class WindowVirtualizerController... extends VirtualizerControllerBaseWindow, ... { constructor(host, options) { super(host, { getScrollElement: () (typeof document ! undefined ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () (typeof document ! undefined ? window.scrollY : 0), ...options, }) } }从 packages/virtual-core/src/index.ts 的源码可以看出这四种内置实现的职责observeElementRect用ResizeObserverborder-box持续观测滚动容器的宽高并支持useAnimationFrameWithResizeObserver选项决定是否延迟到下一帧处理observeElementOffset监听滚动容器的scroll及可选的原生scrollend事件按horizontal/isRtl选项读取scrollLeft或scrollTopelementScroll/windowScroll基于scrollTo({ top/left, behavior })实现程序化滚动WindowVirtualizerController额外把getScrollElement默认指向windowinitialOffset默认读取window.scrollY适合整页滚动场景。固定示例使用的是元素滚动控制器仓库中的 dynamic 示例 也演示了同一 API 在动态尺寸场景下的用法可作为对比参考。固定尺寸选项速查以 virtual-core 为准虚拟器的全部选项定义于 packages/virtual-core/src/index.ts 的VirtualizerOptions接口详细说明见 docs/api/virtualizer.md。固定尺寸示例用到的核心选项及其默认值如下选项类型默认值本例取值说明countnumber必填10000 /sentences.length虚拟化项的总数getScrollElement() TScrollElement \| null必填返回scrollElementRef.value返回滚动容器未挂载时可返回 nullestimateSize(index) number必填() 35/() 100每项尺寸。固定尺寸下直接返回常量overscannumber15视口上下额外渲染的项数越大越不易出现滚动白屏但渲染开销越高horizontalbooleanfalse列/网格的列控制器为 true是否水平滚动paddingStart/paddingEndnumber0未用内容首尾的内边距scrollPaddingStart/scrollPaddingEndnumber0未用scrollToIndex等滚动定位时预留的边距gapnumber0未用项与项之间的间距像素scrollMarginnumber0未用列表起始点与滚动元素起点之间的偏移常用于页头或同页多虚拟器场景lanesnumber1未用多列瀑布流场景的通道数isScrollingResetDelaynumber150未用最后一次滚动事件后重置isScrolling的等待毫秒数useScrollendEventbooleanfalse未用是否改用原生scrollend事件判定滚动结束debugbooleanfalse未用开启调试日志initialOffsetnumber | (() number)0未用首次渲染时的初始滚动位置适合 SSR 与条件渲染从setOptions的实现可以确认上述默认值overscan: 1、paddingStart/End: 0、gap: 0、lanes: 1、isScrollingResetDelay: 150、useScrollendEvent: false等均在此处合并。虚拟项的形态与渲染契约getVirtualItems()返回的每一项是VirtualItem定义于 packages/virtual-core/src/index.ts类型说明见 docs/api/virtual-item.mdexport interface VirtualItem { key: Key // number | string | bigint index: number // 在数据数组中的下标 start: number // 距列表起点的像素偏移 end: number // start size size: number // 该项尺寸垂直高度水平宽度 lane: number // 所在通道lanes 1 时使用 }配合 src/main.ts 的渲染写法可以总结出固定尺寸虚拟化的“渲染契约”外层容器负责滚动overflow: auto并用ref暴露给getScrollElement撑高容器持有position: relative与总尺寸getTotalSize()为绝对定位的子项建立坐标基准每个虚拟项使用position: absolutetransform: translateY/translateX(start)定位尺寸取virtualItem.sizerepeat的 key 使用virtualItem.key默认即 index可用getItemKey覆盖为业务主键。为什么固定尺寸下可以采用“纯估算、不测量”的策略因为estimateSize返回的常量就是真实尺寸虚拟器无需在渲染后调用measureElement回读 DOM。这也解释了为何示例中getVirtualItems()返回的size与start可以直接用于布局——它们基于 10000 个等宽/等高元素的总和精确计算得出滚动过程中元素数量巨大但 DOM 中始终只保留可见窗口附近的一小部分本例overscan: 5即视口外各多渲染 5 项。与动态尺寸、窗口虚拟化的关系理解固定示例后可以把它放入整个仓库示例体系里定位固定 vs 动态固定示例中estimateSize返回常量元素尺寸永不改变dynamic 示例 则让各项尺寸随内容变化需要配合virtualizer.measureElement与ResizeObserver动态测量。两种模式在 API 层面完全一致区别只在estimateSize的实现与是否启用测量。元素 vs 窗口本示例使用VirtualizerController滚动发生在容器元素上若要让整个页面滚动则改用WindowVirtualizerController见 packages/lit-virtual/src/index.ts其接口与用法相同仅底层滚动目标不同。与其他框架示例的关系本仓库为 React、Vue、Svelte、Solid、Angular、Marko 等提供了同构的 fixed/dynamic 示例见 examples 目录tanstack/lit-virtual作为其中之一遵循统一的tanstack/virtual-core引擎因此本文学到的选项语义同样适用于其他框架适配器。小结通过本示例可以确认运行即所得npm install npm run dev即可在本地看到行、列、网格三个固定尺寸虚拟化列表README 中的npm run start与 package.json 实际脚本存在出入运行时以dev为准。固定尺寸的核心是常量估算estimateSize返回固定值配合position: absolutetranslateY/X即可精确布局无需 DOM 测量。一维虚拟器组合出多维结构行、列是同一个 API 的horizontal开关网格是两个控制器共享滚动容器嵌套渲染。适配器是生命周期桥接VirtualizerController通过hostConnected/hostUpdated/hostDisconnected与 Lit 响应式系统对接把 tanstack/virtual-core 的安装、更新与清理接入组件生命周期。进一步阅读适配器完整说明见 docs/framework/lit/lit-virtual.md全部选项与实例方法见 docs/api/virtualizer.md虚拟项结构见 docs/api/virtual-item.md。赞分享前端UI组件【免费下载链接】virtual Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte项目地址https://gitcode.com/gh_mirrors/vi/virtual点击查看免费下载相关推荐Angular 固定尺寸虚拟滚动实战基于 tanstack/angular-virtual 的行、列与网格示例Angular 固定尺寸虚拟滚动实战基于 tanstack/angular virtual 的行、列与网格示例 导读 本指南以仓库中的 Angular fi前端UI组件用 TanStack Virtual 在 Lit 中实现动态尺寸虚拟化列表从行、列到网格的完整实战用 TanStack Virtual 在 Lit 中实现动态尺寸虚拟化列表从行、列到网格的完整实战 导读 在 Web 前端处理上万条数据时一次性把全部 DO前端UI组件Angular 动态尺寸列表虚拟化实战tanstack/angular-virtual 官方示例的运行、构建与源码剖析Angular 动态尺寸列表虚拟化实战tanstack/angular virtual 官方示例的运行、构建与源码剖析 本文围绕仓库中的 Angular 动前端UI组件上一篇CANN ops-nn 算子指南aclnnIndexFill 与 aclnnInplaceIndexFill 接口详解下一篇pymoo多目标决策实战指南从Pareto前沿到最优方案选择的完整流程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考