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 FengMo WenXue