Skip to content

VirtList API

  1. list.item.id
  2. item 元素之间不能使用

StyleValue / ClassValue

  • StyleValue: string | Record<string, string | number | null | undefined> | StyleValue[]
  • ClassValue: string | Record<string, boolean | null | undefined> | ClassValue[]

配置项

参数说明类型默认值是否必须
list数据Array-
itemKey项的 id,(否则会无法正常滚动)String|Number-
estimatedSize预估尺寸Number20-
itemGap元素之间的间距 (元素尺寸包含 itemGap)Number0-
fixedSize是否为固定高度,可以提升性能
注意:动态高度模式下,请勿使用
Booleanfalse-
buffer当渲染量大,滚动白屏严重时,可以给定数值,bufferTop 和 bufferBottom 会等于 bufferNumber0-
bufferTop顶部 buffer 个数Number0-
bufferBottom底部 buffer 个数Number0-
horizontal是否水平滚动Booleanfalse-
edgeThreshold滚动阈值(提前触发 toTop 或 toBottom)单位:pxnumber0-
scrollDuration平滑滚动(behavior: 'smooth')的默认动画时长,单位:msnumber300-
smoothMaxDistance平滑滚动允许逐帧穿越的最大距离(px),超出部分先瞬跳;缺省为两倍视口number两倍视口-
initialIndex挂载后自动定位到的索引Number0-
initialOffset挂载后自动定位到的偏移量(px)Number0-
listStyle列表容器样式StyleValue''-
listClass列表容器类名ClassValue''-
itemStyleitem 容器样式StyleValue''-
itemClassitem 容器类名ClassValue''-
renderControl渲染控制器(begin: number, end: number ) => { begin: number; end: number };--
loadMore触达边界时的取数回调,见下方 分页与无限加载(direction: 'top' | 'bottom') => boolean | void | Promise<boolean | void>--
hasMoreTop顶部方向是否还有更多数据;可覆盖 loadMore 的返回值Booleantrue-
hasMoreBottom底部方向是否还有更多数据;可覆盖 loadMore 的返回值Booleantrue-
initialPosition首屏定位。'bottom' 挂载后定位到底部并随尺寸测量渐进校准;initialIndex / initialOffset 优先'top' | 'bottom''top'-
stickyBottom尾部追加时是否自动跟随到底部;仅在原本就贴底时跟随Booleanfalse-
stickyThreshold判定"贴底"的容差(px),缺省取 edgeThreshold(至少 2px)Number0-
aria为辅助技术补上列表语义。开启后渲染容器加 role(默认 list)、每项加 role="listitem"aria-posinset / aria-setsize——位置与总数取自数据,不由 DOM 元素个数推断Boolean|'list'|'listbox'false-
ariaLabel供辅助技术朗读的列表名称,配合 aria 使用String--
copyText每项对应的纯文本,给出即开启跨虚拟化的选区复制:用户选中的范围跨越未渲染区域时,中间那些没有 DOM 的项从数据补齐。不给则完全不挂 copy 监听(item, index) => string--
copySeparator复制时各项之间的分隔符String'\n'-
scrollbarAutoHideDelay滚动条停止滚动多久后淡出(ms),0 表示常驻Number1200-
scrollbarMinThumbSize滑块的最小长度(px)Number20-
keyboard是否接管键盘(方向键 / PageUp-Down / Space / Home-End)。宿主已在祖先元素上处理这些键时关掉它——库的监听在后代上、先触发,两边都响应会让一次按键既滚一段又移动一格。滚轮 / 触摸不受影响Booleantrue-
crossScroll交叉轴(竖向列表的横向、横向列表的竖向)是否允许滚动。那条轴上没有虚拟化、内容是完整的真实 DOM,所以交给浏览器原生滚动(落在 scrollContentEl 上)。关掉即回到两轴全裁切Booleantrue-

渲染函数

对应框架版的插槽。返回 HTMLElement 会被 append 进容器;也可以直接操作最后一个参数 el(容器本身),少一层 DOM 嵌套。

名称说明签名
renderItemitem 内容(item, index, el) => HTMLElement | void
updateItem数据变更时原地更新已渲染的项。不传则 setList 后同 key 的项内容不刷新(item, index, el) => void
renderHeader顶部区域(参与滚动);第二参数为加载状态(el, loadState) => HTMLElement | void
renderFooter底部区域(参与滚动);第二参数为加载状态(el, loadState) => HTMLElement | void
renderStickyHeader顶部悬浮区域(el) => HTMLElement | void
renderStickyFooter底部悬浮区域(el) => HTMLElement | void
renderEmpty空状态(el) => HTMLElement | void

事件

方法名说明参数
toTop触顶的回调列表中第一项
toBottom触底的回调列表中最后一项
scroll滚动。载荷是普通对象而不是 Event,字段见 VirtScrollEventevent: VirtScrollEvent
offsetChange偏移量变化。要跟着偏移量重写 DOM 时用它,只想感知滚动用 scrolloffset: number
itemResizeItem 尺寸发生变化{ id: string, newSize: number }
update渲染列表更新{ renderList: any[], state: ListState }
loadStateChange加载状态变化(loadState: LoadState)

暴露方法

方法名说明参数
reset重置列表-
clientEl滚动容器 .virt-list__client(主轴视口;交叉轴的 scrollport 是 scrollContentEl)。逃生舱:只认 DOM 元素的场合用它,感知滚动仍用 scroll 事件-
scrollContentEl交叉轴(横向)的滚动容器 .virt-list__scroll-content。读写它的 scrollLeft 就是读写横向滚动位置,横滚是真的原生 scroll 事件-
getOffset获取当前滚动偏移量(容器 scrollTop 恒为 0,必须走这里读)-
getMaxOffset当前允许的最大偏移量,即 getTotalSize() - clientSize-
getTotalSize内容总尺寸(列表项 + 各插槽),对应原生语境的 scrollHeight-
getIndexByOffset给定偏移量落在第几项,getItemPosByIndex(index).top 的逆运算offset
getSlotsTotalSize所有插槽尺寸之。分项数据读 core.slotSize-
scrollToTopscroll to topoptions?: VirtScrollOptions
scrollToBottomscroll to bottomoptions?: VirtScrollOptions
scrollToIndexscroll to indexindex, options?: VirtScrollOptions
scrollIntoViewscroll to index if needed(不在可视范围内)index, options?: VirtScrollOptions
scrollToOffsetscroll to pxpx, options?: VirtScrollOptions
scrollFromUser上报一次用户发起的滚动:触发 toTop / toBottom 与自动续拉,并作废 scrollToIndex 之类的定位意图。接自定义输入源时用它px
cancelScroll取消进行中的平滑滚动动画-
resume把内容重新摆到当前偏移量上。容器被隐藏 / 重新挂载后(keep-alive)用它复位-
onOffsetChange订阅偏移量变化,返回取消订阅的函数。容器不派发原生 scroll,跟着滚动重算的逻辑挂这里(cb: (offset: number) => void) => () => void
getItemSize获取指定 item 尺寸index
getItemPosByIndex获取指定 item 的位置信息: { top: number; current: number; bottom: number;}index
forceUpdate兜底手段:重建全部可见项的 DOM。能指明 key 时优先用 refreshItems-
refreshItems重建指定的项。只在「改了数据又没走 setList」时需要——走了 setList 的话由 updateItem 原地更新,更轻。不传参数等价于 forceUpdate;传 key 数组则只重建那几项,其余 DOM 原样保留(keys?: Array<string | number>) => void
manualRender手动控制渲染(提供渲染起始)(renderBegin: number, renderEnd: number) => void
getState获取状态数据() => ListState
getLoadState获取当前加载状态() => LoadState

estimatedSize 配错会有一次提示

这个值是未测量项的占位尺寸。项渲染出来后一切以实测为准,所以它给错不会让布局 最终出错,但它决定了首屏渲染多少项:给大了视口下半部分会短暂留白, 给小了会白建一堆 DOM。远距离跳转的落位精度、以及尚未测量区间的滚动条长度也依赖它。

因为这类症状很难反推到一个配置项上,组件会在收集到 32 项实测尺寸后比对一次: 实测中位数与配置值相差一倍以上时,向控制台提示一次并给出建议值。 只提示一次,改对了就不再出现。

取中位数而不是均值,是为了让几条超长项不至于把结论带偏——中位数给出的正是 「典型一项有多高」,也就是这个配置该填的值。

头部增删不需要手动补偿

setList 后,组件会自动识别头部的增删并补偿滚动位置,随后完成重算与渲染——头部插入、 头部删除、以及「一端加一页另一端裁一页」这种总长度不变的双向分页,都在覆盖范围内。

识别依据是首项 key 的移动,范围上界 65536 项。超出这个量级的头部改动识别不出来, 视口不再维持——那更可能是整体换数据源,本就不该维持位置。

forceUpdate 同理通常不必调用:列表变更已经带上了重算与通知。

VirtScrollOptions

scrollToIndex / scrollIntoView / scrollToTop / scrollToBottom / scrollToOffset 都接受一个可选的 VirtScrollOptions,用于开启平滑滚动:

字段说明类型默认值
behavior'auto' 瞬时跳转;'smooth' 平滑动画'auto' | 'smooth''auto'
align目标项与视口的对齐方式(仅 scrollToIndex 生效):'start' 项顶部对齐视口顶部;'end' 项底部对齐视口底部'start' | 'end''start'
focus落位后把键盘焦点移到目标项。引用跳转 / 搜索定位这类「程序把用户带到别处」的操作,只动视口不动焦点会让辅助技术用户失去上下文。目标项会被补上 tabindex="-1"Booleanfalse
duration动画时长(ms),缺省取属性 scrollDurationnumber300
maxDistance本次逐帧穿越的最大距离(px),缺省取属性 smoothMaxDistancenumber两倍视口
onDone动画结束回调,canceledtrue 表示被中断(canceled: boolean) => void-
js
// 平滑滚动到第 3000 项
virtList.scrollToIndex(3000, { behavior: 'smooth' });
// 自定义时长并在结束后做点什么
virtList.scrollToTop({ behavior: 'smooth', duration: 600, onDone: (canceled) => {} });

注意:

  1. 不传参数时行为与旧版本完全一致(瞬时跳转)。
  2. 动画期间用户滚动滚轮或触摸滑动会立即接管,动画中断并回调 onDone(true);调用 cancelScroll() 或发起新的滚动调用同样会中断前一个动画。
  3. 平滑动画每帧都会重新计算目标位置,不定高列表在滚动途中撑开高度也不会跑偏;动画正常结束后还会做一次精确落位修正。

长距离滚动为什么会先"瞬跳"一段

虚拟列表逐帧穿越长距离时,相邻两帧的渲染区间完全不重叠 —— 每一帧都要销毁整屏、再新建整屏 DOM,主线程跟不上就会露白,而中间一闪而过的几十屏内容本身也没有观看价值。

所以平滑滚动只逐帧滚过最后一段距离,超出部分先瞬跳掉。这段距离由属性 smoothMaxDistance 控制(也可以在单次调用里用 maxDistance 覆盖),缺省为两倍视口高度:

取值效果
缺省 / 0自动取两倍视口高度(推荐)
具体像素值自定义逐帧穿越的距离,越小越不容易露白
Infinity全程逐帧滚动,长距离跳转会明显露白

配合 buffer 属性(渲染上下额外几项)可以进一步消除滚动边缘的细白条。

align:对齐方式

scrollToIndex 默认让目标项的顶部对齐视口顶部。当目标项比视口还高时(例如展开后有好几屏的长消息),顶部对齐会把刚展开的内容顶到视口外面去,这时候用 align: 'end' 让它的底部贴住视口底部:

js
// 项底部贴住视口底部,露出它的末段
listRef.value.scrollToIndex(index, { align: 'end' });

对齐用的是列表项容器的边界,项内的 padding 与 itemGap 都已计入,所以卡片之间的间隔会自然保留。

两种对齐都带渐进修正:目标项的真实高度往往要等渲染后才测得出来,修正会跟着 ResizeObserver 的每次回调重算目标偏移,直到尺寸稳定。所以不需要等高度落定再调用。

ListState

属性类型说明
itemsTotalSizenumber不包含插槽的高度
leadingSizenumber渲染块的位移量:[0, renderBegin) 的尺寸之和(不含 header)。自建渲染层靠它定位自己的渲染块
inViewBeginnumber可视区起始下标
inViewEndnumber可视区结束下标
renderBeginnumber实际渲染起始下标(包含 buffer)
renderEndnumber实际渲染结束下标(包含 buffer)

slotSize:SlotSize

属性类型说明
clientSizenumber可视区容器高度
headerSizenumberheader 插槽高度
footerSizenumberfooter 插槽高度
stickyHeaderSizenumberstickyHeader 插槽高度
stickyFooterSizenumberstickyFooter 插槽高度

LoadState

header / footer 拿到的加载状态、loadStateChange 的回调参数、以及 getLoadState() 的返回值都是这个类型:

属性类型说明
loadingTopboolean顶部方向正在加载
loadingBottomboolean底部方向正在加载
hasMoreTopboolean顶部方向是否还有更多数据
hasMoreBottomboolean底部方向是否还有更多数据
pendingNewnumber未贴底时尾部新增的项数,用于渲染"N 条新消息"角标;视口回到底部后归零(仅 stickyBottom 开启时累加)

VirtScrollEvent

scroll 事件的载荷。容器是 overflow: hidden,浏览器不会自己滚动,所以这里给的不是原生 Event,而是库内部的偏移量账本;为什么这样设计见指南的 滚动是怎么工作的

字段类型说明
offsetnumber当前偏移量,对应原生语境的 scrollTop
deltanumber相对上一次的位移,正数朝列表尾部;恒不为 0
direction'start' | 'end'本次位移的方向
clientSizenumber视口尺寸,对应原生语境的 clientHeight
scrollSizenumber内容总尺寸,对应原生语境的 scrollHeight
maxOffsetnumberscrollSize - clientSize,不为负
atStartboolean是否已在起始边界
atEndboolean是否已在末尾边界
source'user' | 'program' | 'adjust'本次滚动由谁触发,见下表
source含义
user滚轮 / 键盘 / 触摸 / 拖拽滑块,即 scrollFromUser
programscrollToOffset / scrollToIndex / scrollToTop / scrollToBottom(平滑动画的每一帧也算)
adjust库为了「让用户正在看的内容留在原处」做的内部补偿:头部增删的位移补偿、尺寸测量后的锚点校正、列表变短后的越界回夹、贴底跟随

延伸阅读

  • 分页与无限加载 —— loadMore / hasMoreTop / hasMoreBottom / initialPosition / stickyBottom 的完整用法,以及无限加载、双向分页、聊天室三类场景的写法。
  • 整段复制 —— copyText 的取值规则、什么情况下不介入,以及跨很多屏拖选为什么在任何虚拟滚动实现里都不可靠。
  • 滚动是怎么工作的 —— 为什么容器是 overflow: hidden、 已知取舍、scrollTop 的替代写法、scrolloffsetChange 怎么选、滚动条样式定制。