跳转到内容

Virtualizer 虚拟滚动 ​

只渲染窗口内的条目,列表再长也只绘制可见的几十条。

用法 ​

一万条只渲染可视区内的几条,root 要有确定高度,条目的主轴尺寸由作者按 estimateSize 自行编写

组件结构 ​

加粗的是必需部件。

data-scope="virtualizer":root · viewport · content · item

示例 ​

动态高度 ​

条目开启 measure 后把真实尺寸回传给内核,estimateSize 只是首帧的起点,滚动一遍后即收敛

滚动到指定条目 ​

scrollToIndex 按 align 落位:start 贴上沿、center 居中、end 贴下沿,越界下标由内核夹取

可视区首条:—

横向列表 ​

horizontal 把主轴换为行内轴:位移改写进行首侧,条目宽度由作者编写,gap 由内核直接计入位移

挂载自绘滚动条 ​

滚动容器是视口,提供一个 id 交给滚动条即可;虚拟滚动只管理渲染哪几条,滚动条只负责绘制滚动位置

与无限滚动组成一条长列表 ​

哨兵放置在内容层之后而不是条目之间:窗口外的条目根本没有渲染,放在其中的哨兵永远无法进入可视区

设计指引 ​

何时使用 ​

  • 条目上千甚至上万。
  • 首屏卡顿的根源是 DOM 节点太多。

何时不用 ​

  • 条目只有几十上百条时,虚拟化带来的复杂度不值得。
  • 需要浏览器的页内查找命中所有条目时,未渲染的条目无法被搜索。

特性 ​

  • 支持动态高度(测量而非估算)、横向列表与多列。
  • overscan 决定窗口外多渲染的条数,滚动时不露白。
  • 可以滚到指定条目。

组合 ​

最佳实践 ​

  • 条目高度差异大时使用动态高度模式,不依赖估值。
  • 提供滚动到指定条目的入口,否则用户无法找回之前的位置。

反模式 ​

  • 在虚拟列表内放高度会突变的内容(图片未预留宽高比),滚动时位置跳动。
  • 依赖 Ctrl + F 查找。
  • 把无限滚动的哨兵放进条目之间:窗口外的条目不渲染,哨兵也不渲染,第二页无法获取。

API 参考 ​

产物 ​

层值
自定义元素<xh-virtualizer>
Vue 组件XhVirtualizerContent XhVirtualizerItem XhVirtualizerRoot XhVirtualizerViewport
组合式函数useVirtualizer
状态机virtualizerMachine
皮肤@xihan-ui/styles/virtualizer.css

Props ​

属性类型必填说明
countnumber总条数,默认 0。
estimateSizenumber | ((index: number) => number)每条的估算主轴尺寸(px)。等高列表可以直接提供一个数字。 未提供时按 0 计算:所有条目都会落进窗口,先渲染出来再依靠 measureElement 回填真实尺寸。
overscannumber可视区前后各多渲染的条数,默认 5。
horizontalboolean横向列表(主轴是行内轴),默认 false。
gapnumber相邻两条之间的主轴间距(px),默认 0。位移由内核直接计算,不依靠外边距。
getItemKey(index: number) => string | number条目身份。默认即下标;列表会增删时提供稳定 key,测量缓存才能跟随条目。
onRangeChange(details: VirtualizerRangeChangeDetails) => void应渲染的区间变化。只在快照实际变化时回调,滚动但可见区间未变不会触发。
scrollMarginnumber列表起点距滚动容器起点的距离(px),默认 0。 列表上方还有其他内容(页头、筛选栏)时提供它,否则区间会整体偏移该段距离。
paddingStartnumber列表前后的内边距(px),默认 0。计入总长,第一条从 paddingStart 处起算。
paddingEndnumber
lanesnumber多列网格的列数,默认 1(单列)。条目按下标轮流落到各列上。

事件 ​

自定义元素将载荷放在 detail;Vue 使用同名 emit。

事件载荷说明
range-changeVirtualizerRangeChangeDetails应渲染的区间变化;detail 为 { virtualItems, totalSize, startIndex, endIndex }

插槽 ​

仅列出带载荷的插槽。

Vue 组件插槽载荷说明
XhVirtualizerRootdefaultVirtualizerRootSlotProps

React 适配器 props ​

只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。

React 组件属性类型必填说明
XhVirtualizerItemvaluenumber | string是该节点的下标。
XhVirtualizerItemmeasureboolean是否把真实尺寸回传给内核;未开启时条目尺寸按 estimateSize 计算。
XhVirtualizerRootchildrenSlotChildren<VirtualizerRootSlotProps>

状态 ​

以下名称仅用于内部状态机。

状态:idle · scrolling

事件:SCROLL.START · SCROLL.END · MEASURE

connect API ​

getXxxProps() 返回对应部件的宿主属性。

成员类型说明
virtualItemsreadonly VirtualizerItemState[]当前应渲染的下标,以及它们的位移与尺寸。
totalSizenumber整份列表的主轴总长(px)。
startIndexnumber | null可视区首条下标(不含过扫描);没有任何条目可容纳时为 null。
endIndexnumber | null可视区末条下标(不含过扫描);没有任何条目可容纳时为 null。
horizontalboolean
lanesnumber
scrollingboolean正在滚动。
scrollToIndex(index: number, options?: VirtualizerScrollToOptions) => void滚动到某一条。越界下标由内核夹取。
measureElement(element: HTMLElement | null) => void把条目节点的真实尺寸回填给内核(动态高度使用)。传 null 无副作用。
measure() => void丢弃全部实测尺寸重新按估算值排列。视口更换排版时使用。
getRootProps() => T['element']
getViewportProps() => T['element']
getContentProps() => T['element']
getItemProps(props: VirtualizerItemProps) => T['element']

无障碍 ​

键盘 ​

规格出处:W3C APG

无键盘交互(不接收焦点,或焦点行为完全由原生元素提供)。

样式参考 ​

皮肤 ​

@xihan-ui/styles/virtualizer.css 使用 [data-scope="virtualizer"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides。

数据属性 ​

由 connect 生成;条件不成立时不输出无值属性。

部件属性值
rootdata-orientation'horizontal' | 'vertical'
rootdata-scrolling''(条件成立时才出现)
viewportdata-orientation'horizontal' | 'vertical'
contentdata-orientation'horizontal' | 'vertical'
itemdata-indexprops.index
itemdata-laneitem.lane | undefined
itemdata-orientation'horizontal' | 'vertical'

动效 ​

本组件皮肤不含过渡与关键帧,也没有脚本驱动的动效:状态一变,外观立即到位。

RTL ​

皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。

Released under The MIT License