virtual —— 虚拟滚动
定高虚拟滚动原本在 virtual-list 与 table 各有一份实现——连默认过扫描量都不一样(2 与 3),修一个边界漏另一处。shared/virtual 把算法抽成唯一一份,两处只做薄封装。
- 虚拟列表 / 表格:数据上千行时只渲染视口内的十几行。
- 水平虚拟滚动:
computeWindow是轴无关的,横向时间轴、横向滚动的 tabs 条同样适用。 - 不该用它:不定高场景只有「取偏移」能力(
getItemOffsetAt),没有变高窗口算法——动态高度的虚拟滚动需要 UI 层维护累积高度表,本模块只提供取偏移这一步。
import { computeRange, computeWindow, getItemOffsetAt, LH_VIRTUAL_OVERSCAN, type LhVirtualRange,} from '@lh-ui/share/core/shared/virtual'export interface LhVirtualRange { start: number // 渲染区间起点下标(含) end: number // 渲染区间终点下标(不含) offsetY: number // 起点元素相对容器顶部的偏移(撑高量的负值起点) totalHeight: number // 全部数据的总高度(撑高容器用)}
export interface LhAxisRange { start: number end: number offset: number totalSize: number}
export const LH_VIRTUAL_OVERSCAN = 2 // 默认过扫描行数| 函数 | 签名 | 作用 |
|---|---|---|
computeRange |
(scrollTop, viewportHeight, itemHeight, total, overscan?) => LhVirtualRange |
纵向定高虚拟区间 |
computeWindow |
(scrollOffset, viewportSize, itemSize, total, overscan?) => LhAxisRange |
轴无关定高窗口(横向用) |
getItemOffsetAt |
(items: { offset: number }[], index: number) => number |
动态高度模式取指定项的滚动偏移 |
computeRange 的核心是三步:
- 首项下标:
first = floor(scrollTop / itemHeight),再往前后各扩overscan行。 - 末项下标:
end = min(total, start + visible + overscan × 2),其中visible = ceil(viewportHeight / itemHeight)。 - 总高度:
totalHeight = total × itemHeight,用于把容器撑到真实高度,让滚动条比例正确。
返回的 offsetY 是「渲染起点在总高度里的位置」,UI 层把它作为内容容器的 translateY(或 padding-top)。
这个模块的防御主要针对数据收缩的那一帧:用户正在看第 500 行,数据突然变成 10 条,scrollTop 还没来得及被浏览器钳回去,朴素的算法会算出 start = 490 > end,slice() 返回空数组 → 白屏。
处理方式:first 钳到 total - 1,保证 start <= end 永远成立。同类守卫还有:
| 坏输入 | 行为 |
|---|---|
itemHeight 非有限 / ≤ 0 |
返回全零区间(不渲染,不报错) |
total 非有限 / ≤ 0 |
返回全零区间 |
scrollTop 非有限 |
回退 0 |
viewportHeight 非有限 |
回退 0(→ 只渲染 overscan 部分,不白屏) |
overscan 非有限 |
回退 LH_VIRTUAL_OVERSCAN |
computeRange(400, 300, 40, 1000)// first = 10, visible = 8, overscan = 2// start = 10 - 2 = 8, end = min(1000, 10 + 8 + 4) = 22// offsetY = 8 * 40 = 320, totalHeight = 40000// → 渲染第 8~21 项,容器 translateY 320pxVue3 接线
Section titled “Vue3 接线”<template> <div class="lh-virtual-list" @scroll="onScroll" :style="{ height: `${viewportHeight}px` }"> <div :style="{ height: `${range.totalHeight}px` }"> <div :style="{ transform: `translateY(${range.offsetY}px)` }"> <LhVirtualItem v-for="i in visibleItems" :key="i.index" :data="i.data" /> </div> </div> </div></template>
<script setup lang="ts">const range = ref<LhVirtualRange>({ start: 0, end: 0, offsetY: 0, totalHeight: 0 })
function onScroll(e: Event) { range.value = computeRange( (e.target as HTMLElement).scrollTop, viewportHeight.value, props.itemHeight, props.data.length, )}</script>// 同一份算法,换轴const window = computeWindow(scrollLeft, viewportWidth, itemWidth, total)滚动到指定项(动态高度)
Section titled “滚动到指定项(动态高度)”// UI 层维护累积偏移表 [{ offset: 0 }, { offset: 40 }, ...]const offset = getItemOffsetAt(itemOffsets, targetIndex)container.scrollTop = offset- 定高是前提:
computeRange假设每项高度严格相等。若列表项高度可变,UI 层要自己维护高度表,并用getItemOffsetAt做跳转——本模块不提供变高窗口算法。 getItemOffsetAt越界返回 0:空列表、下标越界都是 0,不抛错。- 过扫描的取舍:
LH_VIRTUAL_OVERSCAN = 2是「快速滚动时不闪」与「多渲染行数」的平衡点;用户快速滚动时overscan区域保证下一帧的内容已经渲染好。可以通过参数覆盖,但小于 1 会在滚动时闪烁。 computeWindow与computeRange同源:前者内部委托后者再重命名字段,行为完全一致,纵向场景用哪个都行。
@lh-ui/share/core/virtual → computeRange( )返回值
(填好参数后点运行)Copyright © 2024-2026 风默文学 | 宁ICP备2024006574号 |
苏公网安备32040002010537