ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vuetify 无限滚动组件 `v-infinite-scroll` 完全指南:自动/手动加载、双向滚动与虚拟化实战

Vuetify 无限滚动组件 `v-infinite-scroll` 完全指南:自动/手动加载、双向滚动与虚拟化实战 Vuetify 无限滚动组件v-infinite-scroll完全指南自动/手动加载、双向滚动与虚拟化实战【免费下载链接】vuetify Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetifyv-infinite-scroll是 Vuetify 内置的无限滚动容器组件用于在用户滚动接近内容末尾时按需加载更多条目非常适合展示数量未知且庞大的数据列表避免一次性渲染全部内容导致性能下降。本文将以 Vuetify 官方文档infinite-scroller.md为核心骨架结合组件源码VInfiniteScroll.tsx与全部官方示例packages/docs/src/examples/v-infinite-scroll/系统讲解其核心用法、全部 Props/Slots、reset()暴露方法以及虚拟化无限列表的进阶方案。读完本文你将能够独立实现自动加载、手动加载更多、顶部/底部/双向滚动等多种无限列表场景。组件概览一个可无限延伸的滚动容器从文档定义来看v-infinite-scroll是一个容器组件当用户滚动接近容器边缘时组件会触发加载逻辑把新内容追加进列表。它同时支持垂直与水平两种滚动方向且可以通过mode属性在自动加载与手动加载之间切换。组件由以下部分组成对应文档 Anatomy 章节元素 / 区域说明1. Container容器承载列表内容的滚动容器对应默认插槽2. Loader加载器容器顶部/底部的内容加载区域根据状态渲染加载指示器、空状态或错误提示组件渲染结构见 VInfiniteScroll.tsx在默认插槽前后各放置了一个v-infinite-scroll__side区域用于渲染加载状态在intersect模式下还会在两侧插入VInfiniteScrollIntersect哨兵元素用于监听滚动是否到达边界。快速开始基础用法与load事件滚动接近底部时组件会自动加载更多条目用户也可以切换为手动模式通过点击按钮加载。以下是官方 Usage 示例的核心代码完整文件见 usage.vuetemplate v-infinite-scroll height300 loadload template v-for(item, index) in items :keyitem div :class[pa-2, index % 2 0 ? bg-grey-lighten-2 : ] Item number {{ item }} /div /template /v-infinite-scroll /template script setup import { ref } from vue const items ref(Array.from({ length: 30 }, (k, v) v 1)) async function api () { return new Promise(resolve { setTimeout(() { resolve(Array.from({ length: 10 }, (k, v) v items.value.at(-1) 1)) }, 1000) }) } async function load ({ done }) { // Perform API call const res await api() items.value.push(...res) done(ok) } /scriptload事件的参数对象当组件需要加载更多内容时会触发load事件事件声明见 VInfiniteScroll.tsx回调参数是一个包含两个属性的对象side告知新内容应添加到哪一侧取值为start或end。示例中用它决定向数组头部unshift还是向尾部push。done加载完成后的回调函数接收单个参数status来描述加载结果。其取值如下表Status说明ok内容已成功添加error添加内容时出错。此时会显示error插槽empty没有更多内容可获取。此时会显示empty插槽loading内容正在加载中。会显示一条加载中的提示。该状态仅由组件内部设置不应通过done函数传入需要特别注意loading状态是组件内部在触发load事件前自行设置的见 VInfiniteScroll.tsx业务代码中只需在异步请求完成后调用done(ok | error | empty)即可。组件会依据done返回的状态分别渲染加载器、错误提示或空状态。Props 详解定制加载行为文档明确列出了v-infinite-scroll用于定制行为的几个核心属性其类型声明与默认值集中在 makeVInfiniteScrollProps 中可归纳为下表Prop类型默认值可选值说明modeStringintersectintersect/manual加载触发方式自动滚动接近末尾或手动点击按钮directionStringverticalvertical/horizontal滚动方向sideStringendstart/end/both新内容出现的位置colorString—任意颜色默认加载更多按钮与加载中旋转指示器的颜色marginNumber/String—任意长度值触发加载的边距可理解为提前多少距离开始加载loadMoreTextString$vuetify.infiniteScroll.loadMore任意字符串或 i18n key手动模式默认按钮文案emptyTextString$vuetify.infiniteScroll.empty任意字符串或 i18n key空状态默认文案此外组件还继承了makeDimensionProps()如height、width等尺寸属性与makeTagProps()自定义根元素标签默认div因此可以直接通过height300为滚动容器设定固定高度——无限滚动通常需要给容器一个确定的高度或宽度才能形成可滚动的溢出区域。Mode自动加载与手动加载默认行为modeintersect是滚动条接近末尾时自动尝试加载更多内容。文档同时支持手动模式modemanual此时需要用户主动交互默认是一个按钮才能触发加载。完整示例见 prop-mode.vuetemplate v-infinite-scroll height300 modemanual loadload template v-for(item, index) in items :keyitem div :class[px-2, index % 2 0 ? bg-grey-lighten-2 : ] Item number {{ item }} /div /template /v-infinite-scroll /template script setup import { ref } from vue const items ref(Array.from({ length: 50 }, (k, v) v 1)) function load ({ done }) { setTimeout(() { items.value.push(...Array.from({ length: 10 }, (k, v) v items.value.at(-1) 1)) done(ok) }, 1000) } /script手动模式下默认的加载更多按钮可以被load-more插槽完全替换见下文 Slots 章节。Direction垂直与水平滚动v-infinite-scroll同时支持垂直与水平滚动。只需将direction设为horizontal即可示例见 prop-direction.vuev-infinite-scroll directionhorizontal loadload !-- 内容列表例如横向排列的卡片 -- /v-infinite-scroll从源码实现看方向不仅影响 CSS 类名v-infinite-scroll--vertical/v-infinite-scroll--horizontal还决定组件内部读写滚动量时使用哪一组属性垂直方向操作scrollTop/scrollHeight/clientHeight水平方向操作scrollLeft/scrollWidth/clientWidth见 VInfiniteScroll.tsx。Side控制新内容出现的位置默认情况下组件假定新内容追加到已有内容的末尾sideend但也支持将内容添加到开头sidestart以及开头与末尾同时加载sideboth。使用start侧时滚动条初始位于内容的底部因为新内容总是插到最前面用户向上滚动查看更早的内容。使用both侧时滚动条初始位于内容的中间。sidestart的完整示例见 prop-side-start.vuetemplate v-infinite-scroll height300 sidestart loadload template v-for(item, index) in items :keyitem div :class[px-2, index % 2 0 ? bg-grey-lighten-2 : ] Item number {{ item }} /div /template /v-infinite-scroll /template script setup import { ref } from vue const items ref(Array.from({ length: 50 }, (k, v) v 1)) function load ({ done }) { setTimeout(() { items.value.unshift(...Array.from({ length: 10 }, (k, v) items.value[0] - (10 - v))) done(ok) }, 1000) } /script注意向start侧添加内容时应使用unshift或在both模式下根据side参数决定unshift还是push并保证新数据的数值/排序方向正确。sideboth的示例见 prop-side-both.vue其load处理函数需要根据side参数分别处理function load ({ side, done }) { setTimeout(() { if (side start) { const arr Array.from({ length: 10 }, (k, v) items.value[0] - (10 - v)) items.value [...arr, ...items.value] } else if (side end) { const arr Array.from({ length: 10 }, (k, v) items.value.at(-1) 1 v) items.value [...items.value, ...arr] } done(ok) }, 1000) }在源码中组件挂载完成后会根据side自动调整初始滚动位置见 VInfiniteScroll.tsxstart直接滚动到最底部both则滚动到(scrollSize - containerSize) / 2即内容中点。Color着色加载控件默认的加载更多按钮与加载中的旋转指示器VProgressCircular都可以通过color属性着色示例见 prop-color.vuev-infinite-scroll colorsecondary height400 modemanual loadload !-- 内容列表 -- /v-infinite-scroll从渲染逻辑renderSide可以看到color会同时传递给默认的VBtnoutlined 风格与VProgressCircular指示器。Slots 详解完全掌控加载状态的表现v-infinite-scroll通过一组插槽让你可以完全自定义各状态的展示。插槽的slotProps提供{ side, props }其中props内含onClick触发加载的回调与color可直接通过v-bindprops绑定到你的自定义按钮上。插槽展示时机default容器内的列表内容load-moremodemanual且状态不是loading时显示的加载控件loadingmodemanual且状态为loading时显示的加载中提示empty状态为empty时显示的空状态提示error状态为error时显示的错误提示Loading 插槽自定义加载中的提示文案示例见 slot-loading.vuev-infinite-scroll height400 loadload !-- 内容列表 -- template v-slot:loading This is taking a very long time... /template /v-infinite-scroll示例中load函数延迟 4000ms 才调用done(ok)便于观察自定义 loading 文案的展示效果。若未提供loading插槽自动模式与手动模式默认都会渲染一个带color的VProgressCircularindeterminate旋转指示器。Load-more 插槽手动模式下自定义触发加载的操作控件示例见 slot-load-more.vuev-infinite-scroll height400 modemanual loadload !-- 内容列表 -- template v-slot:load-more{ props } v-btn iconmdi-refresh sizesmall varianttext v-bindprops /v-btn /template /v-infinite-scroll这里的v-bindprops会把组件内置的onClick与color绑定到你的自定义按钮上点击后即触发intersecting(side)开始加载。Empty 插槽自定义没有更多内容的空状态提示示例见 slot-empty.vue。当load中调用done(empty)后显示v-infinite-scroll height400 loadload !-- 内容列表 -- template v-slot:empty v-alert typewarningNo more items!/v-alert /template /v-infinite-scroll若未提供empty插槽则默认渲染由emptyText属性或对应 i18n 文案指定的文本。Error 插槽当done(error)被调用时显示错误插槽示例见 slot-error.vue。通常会在错误提示旁附带一个重试按钮复用插槽提供的propstemplate v-slot:error{ props } v-alert typeerror div classd-flex justify-space-between align-center Something went wrong... v-btn colorwhite sizesmall variantoutlined v-bindprops Retry /v-btn /div /v-alert /template点击Retry会再次触发load实现错误后的重试机制。组件暴露的方法reset()v-infinite-scroll通过组件实例暴露reset()方法实现见 VInfiniteScroll.tsx用于在到达empty状态后程序化地将状态重置回默认从而让load可以被再次触发。方法签名与行为如下reset()不带参数时按当前side属性重置sideboth时两侧同时重置。reset(side)可传入start、end或both只重置指定一侧。典型场景服务端数据新增了记录希望重新拉取已到达末尾的列表。官方示例 misc-reset.vue 完整演示了这一能力script setup import { ref, useTemplateRef } from vue const infiniteScrollRef useTemplateRef(scroll) const items ref([]) const showEmptyText ref(false) let firstId 0 let lastId 0 const serverItems ref(Array.from({ length: 30 }, () lastId)) function prependFewMore () { serverItems.value [...serverItems.value, ...Array.from({ length: 6 }, () --firstId)] } function appendFewMore () { serverItems.value [...serverItems.value, ...Array.from({ length: 6 }, () lastId)] } function reset (side) { infiniteScrollRef.value?.reset(side) } async function load ({ side, done }) { await new Promise(resolve setTimeout(resolve, 500)) let page [] if (side start) { page loadPreviousPage() if (page.length) { items.value [...page, ...items.value] } } if (side end) { page loadNextPage() if (page.length) { items.value [...items.value, ...page] } } done(page.length 10 ? ok : empty) } function loadPreviousPage () { const cursor items.value.at(0) ?? 0 return serverItems.value.filter(x x cursor).reverse().slice(0, 10) } function loadNextPage () { const cursor items.value.at(-1) ?? 0 return serverItems.value.filter(x x cursor).slice(0, 10) } /scripttemplate v-container div classd-flex ga-3 mb-2 v-chipServer items: {{ serverItems.length }}/v-chip v-chipLoaded items: {{ items.length }}/v-chip v-spacer/v-spacer v-checkbox v-modelshowEmptyText hide-details labelshow empty text/v-checkbox /div v-infinite-scroll refscroll :empty-textshowEmptyText ? No more records : height300 sideboth loadload template v-for(item, index) in items :keyindex div :class[px-2, index % 2 0 ? bg-grey-lighten-2 : ] Item number {{ item }} /div /template /v-infinite-scroll div classd-flex ga-3 mt-2 v-btn clickprependFewMore(); reset(start)prepend items reset(start)/v-btn v-btn clickappendFewMore(); reset(end)append items reset(end)/v-btn v-btn clickreset()just reset()/v-btn /div /v-container /template该示例还展示了emptyText的用法通过:empty-text传入字符串即可覆盖默认空状态文案也可以传空字符串关闭文案显示。进阶实战虚拟化无限滚动官方文档还提供了一个重要的进阶方案——虚拟化无限滚动示例见 misc-virtual.vue。核心思路是当列表项尺寸均匀一致时无论滚动多远都只渲染固定数量的小部分条目从而获得与列表总长度无关的稳定渲染开销。script setup import { nextTick, ref } from vue const infinite ref() const size ref(300) const virtualLength ref(12) const cards ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) function createRange (length, start) { return Array.from({ length }).map((_, i) i start) } function load ({ side, done }) { const halfVirtualLength virtualLength.value / 2 if (side start) { const arr createRange(halfVirtualLength, cards.value[0] - halfVirtualLength) cards.value [...arr, ...cards.value.slice(0, halfVirtualLength)] nextTick(() { infinite.value.$el.scrollTop infinite.value.$el.scrollHeight - (halfVirtualLength * size.value) - infinite.value.$el.scrollTop }) } else { const arr createRange(halfVirtualLength, cards.value.at(-1) 1) cards.value [...cards.value.slice(halfVirtualLength), ...arr] } done(ok) } /scripttemplate v-infinite-scroll refinfinite height500 sideboth loadload div template v-forcard in cards :keycard v-sheet :colorcard % 2 0 ? primary : card % 4 0 ? secondary : warning :heightsize classd-flex align-center justify-center div{{ card }}/div /v-sheet /template /div /v-infinite-scroll /template实现要点维护一个固定长度的窗口virtualLength 12始终只渲染这 12 个卡片。向end侧滚动时丢弃窗口前一半cards.value.slice(halfVirtualLength)在尾部追加新生成的 6 个卡片。向start侧滚动时在头部插入新生成的 6 个卡片只保留原窗口前一半随后用nextTick在 DOM 更新后手动修正scrollTopscrollHeight - 半窗口高度 - 当前 scrollTop保证视觉位置不跳动。该方案的适用前提是条目尺寸一致示例中所有v-sheet高度均为size这样才能用高度直接计算滚动补偿。若条目高度不固定则需要结合v-virtual-scroll等其他虚拟滚动方案。源码原理IntersectionObserver 与状态机从源码层面理解组件内部机制VInfiniteScroll.tsx有助于正确使用和排查问题1. 哨兵元素 IntersectionObserver 触发加载intersect模式下组件在内容两侧各渲染一个不可见的VInfiniteScrollIntersect哨兵元素类名v-infinite-scroll-intersect样式见 VInfiniteScroll.sass。哨兵内部通过useIntersectionObserverintersectionObserver.ts监听自身是否进入视口一旦进入就向父组件发出intersect事件并触发加载见 VInfiniteScroll.tsx。margin属性会通过 CSS 变量--v-infinite-margin-size作用到哨兵上从而实现提前 N 距离开始加载。2. 双端独立状态机组件为start与end两侧分别维护startStatus/endStatus两个状态初始均为oksideboth时同步更新两侧setStatus / getStatus。触发加载前会先检查mode manual时不自动触发状态为empty或loading时不再重复触发intersecting。3. 滚动位置补偿向start侧插入新内容后如果不修正滚动位置用户会看到内容突然跳动。组件在done(ok)后的nextTick中计算getScrollSize() - previousScrollSize getScrollAmount()并回写scrollTop/scrollLeft进行补偿见 VInfiniteScroll.tsx。reset()方法内部也使用了相同的补偿逻辑。4. 三次requestAnimationFrame的细节在非手动模式下完成一次加载后组件会嵌套三次window.requestAnimationFrame再重新调用intersecting。源码注释对应 issue #17475说明浏览器需要 23 个动画帧后 IntersectionObserver 才会在哨兵离开视口后再次触发回调这是为了确保在内容高度不足一屏时能继续自动加载。5. 本地化文案loadMoreText与emptyText的默认值指向$vuetify.infiniteScroll下的 i18n key英文默认文案定义在 en.tsinfiniteScroll: { loadMore: Load more, empty: No more, },如果应用中配置了其他 locale如中文zh-Hans组件会自动显示对应语言的文案也可以通过loadMoreText/emptyText属性直接覆盖。组件自带单元测试见 packages/vuetify/src/components/VInfiniteScroll/tests/API 元数据与 props 文档生成定义位于 packages/api-generator/src/locale/en/VInfiniteScroll.json可进一步查阅每个属性的完整描述。小结v-infinite-scroll以滚动触发 → 状态机管理 → 插槽渲染的简洁架构覆盖了无限列表的全部常见形态自动/手动加载、垂直/水平方向、单侧/双侧追加、空态/错误态展示以及reset()程序化复位。配合本文介绍的虚拟化窗口技巧即使数据量极大也能保持流畅。官方文档中列出的相关组件Lists、Data tables、Data iterators中同样大量运用了该组件可作为真实场景的进一步参考。动手建议在 Vuetify 项目中直接复制本文的 Usage 示例即可得到可运行的最小无限列表需要分页/游标加载时参考misc-reset.vue的分页逻辑需要超大列表时再应用misc-virtual.vue的虚拟化窗口方案。【免费下载链接】vuetify Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表