- UI组件
- 前端
【免费下载链接】varlet
A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.
var-list是 Varlet(基于 Material Design 2/3 的 Vue 3 组件库)中用于实现无限滚动列表的核心组件。它通过监听滚动容器实现"触底自动加载",支持加载、失败、加载完成三种状态的自定义展示,并可与下拉刷新(PullRefresh)、标签页(TabItem)等组件联动。本文基于组件官方文档与仓库源码,系统讲解其使用方式、状态管理协议与触底检测的底层实现。
组件能力总览
根据 组件文档,var-list提供以下核心能力:
- 无限滚动加载:检测到滚动容器滚动到底部时自动触发
load事件; - 手动位置检查:通过实例方法
check手动触发位置检测,触底时触发load事件; - 状态展示:内置加载状态(loading)、错误状态(error)、数据加载完成状态(finished)的展示区域,且均支持通过插槽或文本属性自定义。
组件源码位于 packages/varlet-ui/src/list/,入口文件 index.ts 通过withInstall注册组件,并导出listProps供按需引入与默认值定制使用。
基本使用:load 事件与 loading 状态协议
触底加载的核心交互协议是:当检测到滚动容器滚动到底部时触发load事件,组件会同时将loading置为true;加载结束时必须手动将loading置回false,本轮加载才算结束。这是防止重复触发的关键——loading为true期间组件不会再次触发加载(见下文源码分析)。
<script setup> import { ref } from 'vue' const loading = ref(false) const finished = ref(false) const list = ref([]) function load() { setTimeout(() => { for (let i = 0; i < 20; i++) { list.value.push(list.value.length + 1) } loading.value = false if (list.value.length >= 60) { finished.value = true } }, 1000) } </script> <template> <var-list :finished="finished" v-model:loading="loading" @load="load" > <var-cell :key="item" v-for="item in list"> List Item: {{ item }} </var-cell> </var-list> </template>要点说明:
v-model:loading双向绑定加载状态,触发load时组件内部会自动将其置为true;finished为只读属性,表示数据已全部加载完成,置为true后组件停止触发load,并展示finished-text文案;- 官方示例代码与仓库内演示 example/index.vue 中
load的实现逻辑一致:每次追加 20 条数据,累计到 60 条后标记finished。
加载失败:error 状态与点击重试
当数据请求失败时,可用v-model:error手动置位错误状态。组件会展示错误提示文案,点击该提示会由组件自动将error复位为false,并重新触发load事件,从而实现"点击重试"的交互。
<script setup> import { ref } from 'vue' const loading = ref(false) const error = ref(false) const list = ref([]) function load() { setTimeout(() => { if (list.value.length === 40) { error.value = true loading.value = false return } for (let i = 0; i < 20; i++) { list.value.push(list.value.length + 1) } loading.value = false }, 1000) } </script> <template> <var-list v-model:error="error" v-model:loading="loading" @load="load" > <var-cell :key="item" v-for="item in list"> List Item: {{ item }} </var-cell> </var-list> </template>"点击重试"的实现位于 List.vue:error插槽的默认渲染节点绑定了@click="load",而load方法内部会先调用onUpdate:error(false)复位错误状态,再调用onUpdate:loading(true)进入加载态,最后触发onLoad:
// packages/varlet-ui/src/list/List.vue function load() { call(props['onUpdate:error'], false) call(props['onUpdate:loading'], true) call(props.onLoad) }这个顺序保证了无论处于何种状态,重新发起加载时错误提示都会被清除。
自定义状态文案:loading-text / finished-text / error-text
三种状态区域均有默认文案,且都可以通过属性覆盖:
| 属性 | 含义 | 默认值(英文 locale) |
|---|---|---|
loading-text | 加载中文案 | Loading |
finished-text | 加载完成文案 | No more |
error-text | 加载失败文案 | Load fail |
默认文案并非写死在组件中,而是来自 locale 文件 locale/en-US.ts 中的listLoadingText、listFinishedText、listErrorText三个词条(中文环境默认为"加载中 / 没有更多了 / 加载失败",见 locale/zh-CN.ts)。List.vue 中的渲染优先级为:显式传入的loadingText ?? (pt ? pt : t)('listLoadingText'),即属性 > LocaleProvider 注入文案 > 全局 locale 默认文案,因此接入 LocaleProvider 的项目可统一切换文案而无需逐个设置属性。
<script setup> import { ref } from 'vue' const loading = ref(false) const finished = ref(false) const list = ref([]) function load() { setTimeout(() => { for (let i = 0; i < 20; i++) { list.value.push(list.value.length + 1) } loading.value = false if (list.value.length >= 60) { finished.value = true } }, 1000) } </script> <template> <var-list loading-text="loading QAQ" finished-text="finished ORZ" error-text="error TNT" :finished="finished" v-model:loading="loading" @load="load" > <var-cell :key="item" v-for="item in list"> List Item: {{ item }} </var-cell> </var-list> </template>与 PullRefresh 组合:上拉加载 + 下拉刷新
将var-list嵌套在var-pull-refresh中即可同时获得"上拉加载"与"下拉刷新"两种交互,注意保证PullRefresh容器的高度不为0,否则下拉手势无法正确识别:
<script setup> import { ref } from 'vue' const refreshing = ref(false) const loading = ref(false) const list = ref([]) function refresh() { setTimeout(() => { console.log('refresh') refreshing.value = false }, 2000) } function load() { setTimeout(() => { for (let i = 0; i < 20; i++) { list.value.push(list.value.length + 1) } loading.value = false }, 1000) } </script> <template> <var-pull-refresh v-model="refreshing" @refresh="refresh"> <var-list v-model:loading="loading" @load="load"> <var-cell :key="d" v-for="d in list">ListItem {{ d }}</var-cell> </var-list> </var-pull-refresh> </template>该组合在 PullRefresh 文档 的"组合"一节中同样被推荐为下拉刷新 + 无限滚动的标准方案。
源码解析:触底检测是如何实现的
阅读 List.vue 可以完整还原"触底加载"的三条技术路径。
1. 定位滚动容器:getParentScroller
组件挂载时(onSmartMounted)调用getParentScroller(listEl.value)向上遍历父节点,找到最近一个overflow-y为scroll或auto的元素作为监听目标;若一直找不到,则回退为window。实现位于 utils/elements.ts:
export function getParentScroller(el: HTMLElement): HTMLElement | Window { let element = el while (element) { if (!element.parentNode) { break } element = element.parentNode as HTMLElement if (element === document.body || element === document.documentElement) { break } const scrollRE = /(scroll|auto)/ const { overflowY, overflow } = getStyle(element) if (scrollRE.test(overflowY) || scrollRE.test(overflow)) { return element } } return window }因此var-list既可以放在页面级滚动(window滚动)中,也可以放在局部滚动容器(如固定高度 +overflow-y: auto的面板)中,二者都能正确触发触底检测。组件卸载时通过onSmartUnmounted(removeScrollerListener)移除scroll监听,避免内存泄漏。
2. 触底判定:detector 元素与 offset
组件模板末尾始终渲染一个检测节点<div :class="n('detector')">(配合 list.less 中width: 100%的零高度样式),它始终位于列表内容的最底部。触底判定逻辑如下:
// packages/varlet-ui/src/list/List.vue function isReachBottom() { const { bottom: containerBottom } = getRect(scroller) const { bottom: detectorBottom } = getRect(detectorEl.value!) // The fractional part of the detectorBottom when bottoming out overflows return Math.floor(detectorBottom) - toPxNum(props.offset) <= containerBottom }其判定条件是:detector 的可视区底部坐标减去offset后不超过滚动容器的可视区底部坐标。源码注释特意指出Math.floor的存在是修复触底时底部坐标溢出小数部分的边界问题——即浮点误差可能让判定多触发一次,向下取整可消除该偏差。
关于offset:其类型是string | number,默认0,表示"距离底部多远时提前触发加载"(预加载距离)。从源码看,字符串值会经过 toPxNum 统一换算,支持10、'10'、'10px'以及vw/vh/rem等单位,因此传入offset="200"可实现在距底部 200px 时提前加载下一页。
3. 检查的触发时机与互斥守卫
check是位置检查的入口(也是组件实例暴露的方法),存在三类触发来源:
// packages/varlet-ui/src/list/List.vue watch(() => [props.loading, props.error, props.finished], check) onSmartMounted(() => { scroller = getParentScroller(listEl.value!) scroller.addEventListener('scroll', check) if (props.immediateCheck) { check() } }) async function check() { await nextTick() if (props.loading || props.finished || props.error || tabItem?.current.value === false || !isReachBottom()) { return } load() }- 滚动监听:滚动容器每次
scroll事件都会调用check(无节流,依赖守卫条件短路); - 初始化检查:
immediate-check默认true,挂载后立即check一次——若首屏内容不足一屏(初始即触底),会立刻触发首次加载,避免用户"拉不动"; - 状态变化重检:
loading/error/finished任一变化都会重新check。例如加载完成追加数据后若仍触底,会自动继续加载下一页。
守卫条件loading || finished || error || tabItem?.current.value === false || !isReachBottom()保证了:
- 同一时间只有一个进行中的加载(
loading互斥); - 加载完成(
finished)后不再响应任何触底; - 处于错误态时等待用户点击重试,而不是静默重发;
- 嵌套在
TabItem中且该页签不可见时不加载(见下节)。
check内部await nextTick()后再取坐标,确保在 DOM 更新(如新数据渲染、状态区显隐)之后再计算,避免基于过期布局误判。
与 TabItem 联动的隐藏机制
从源码结构看,var-list与var-tab-item存在内置联动:list/provide.ts 通过useTabItem从父级TabItem注入 provider,并在 List.vue 中监听tabItem.current:
const { tabItem, bindTabItem } = useTabItem() if (tabItem) { watch(() => tabItem.current.value, check) }这意味着:当var-list位于var-tabs-items的某个var-tab-item内时,隐藏页签中的列表不会触发加载(守卫条件tabItem?.current.value === false),切换到该页签时(current变为true)会自动check一次,若已触底则加载。测试用例 index.spec.js 的load event while list in tab-item验证了这一行为:初始状态下只有激活页签的load被调用一次,切换到第二个页签后其load才被触发。使用var-tabs-items+var-tab-item承载多个分页列表时,这一机制无需任何额外配置即可生效。
使用注意:滚动容器的识别陷阱
官方文档"Be Careful"一节指出了一条重要的 CSS 边界问题:
触底检测依赖监听滚动容器的 scroll 事件。滚动容器是指
overflow-y为auto或scroll的最近元素。当把某元素的overflow-x设置为非visible的值时,浏览器会将该元素的overflow-y自动固化为auto以维持 BFC 结构——这会被误识别为滚动容器,应避免。
对照 getParentScroller 的实现可以确认:它只检查overflow-y和overflow两个计算样式,/(scroll|auto)/命中即返回。因此若列表外层存在一个overflow-x: hidden(浏览器会把overflow-y计算为auto)的包裹元素,且该元素出现在body之前,var-list就会把它的父级而非真正的滚动目标当作 scroller,导致触底检测失效。实践建议:在列表到body的路径上,避免出现非visible的overflow-x声明。
API 参考
Props
| Prop | 说明 | 类型 | 默认值 |
|---|---|---|---|
v-model:loading | 加载状态 | boolean | false |
v-model:error | 错误状态 | boolean | false |
immediate-check | 初始化时是否立即进行位置检测 | boolean | true |
finished | 是否加载完成 | boolean | false |
offset | 距底部触发距离 | string | number | 0 |
loading-text | 加载中文案 | string | Loading |
finished-text | 加载完成文案 | string | No more |
error-text | 加载失败文案 | string | Load fail |
默认值以 props.ts 源码为准(immediateCheck默认true、offset默认0,其余布尔状态默认false);文案默认值来自 locale/en-US.ts。
Methods
| 方法 | 说明 | 参数 | 返回值 |
|---|---|---|---|
check | 触发位置检查,触底时触发load事件 | - | - |
check通过setup的返回值暴露在组件实例上(List.vue),适合在数据手动更新、路由切换后强制重检一次是否触底。
Events
| 事件 | 说明 | 参数 |
|---|---|---|
load | 触底时触发 | - |
Slots
| 插槽名 | 说明 | 插槽参数 |
|---|---|---|
default | 列表内容 | - |
loading | 加载中内容 | - |
error | 加载失败内容 | - |
finished | 加载完毕内容 | - |
四个插槽在 List.vue 模板 中均为条件渲染:loading为真时渲染loading插槽(默认内容是文案 +var-loading迷你转圈),finished/error同理。需要完全自定义状态区 UI(例如替换为骨架屏或自定义按钮)时使用对应命名插槽即可。
样式变量
组件样式定义在 list.less,通过 CSS 变量控制三个状态区的外观,可配合 StyleProvider 组件 定制:
| 变量名 | 默认值 |
|---|---|
--list-loading-height | 50px |
--list-finished-height | 50px |
--list-error-height | 50px |
--list-loading-color | #888 |
--list-finished-color | #888 |
--list-error-color | #888 |
--list-loading-font-size | var(--font-size-md) |
--list-finished-font-size | var(--font-size-md) |
--list-error-font-size | var(--font-size-md) |
例如将加载区调矮并换色:
:root { --list-loading-height: 40px; --list-loading-color: var(--color-primary); }测试用例对行为的佐证
单元测试tests/index.spec.js 从三个层面验证了上述机制:
- 触底触发:mock
getBoundingClientRect返回bottom: 0(即 detector 已触底)后,onLoad与onUpdate:loading各被调用 1 次,验证了"触底 → 自动置 loading → 触发 load"的完整协议; - 三种状态区:分别以
loading/error/finished为真时渲染对应的.var-list__loading/.var-list__error/.var-list__finished节点并展示自定义文案,置回false后节点移除; - TabItem 联动:两个页签各含一个
var-list,仅激活页签的列表触发加载,切换页签后新页签列表才触发加载。
小结
var-list的用法可以浓缩为一条状态协议:监听load事件发起请求 → 请求结束置loading = false→ 数据耗尽置finished = true→ 请求异常置error = true并等待点击重试。理解其源码实现后还有三点值得记住:滚动容器通过overflow-y自动向上查找、触底判定依赖底部的零高度 detector 元素与offset预加载距离、overflow-x的非visible值会污染滚动容器识别。掌握这些细节,可以在移动端信息流、分页接口、多页签列表等场景下稳定落地无限滚动加载。
- UI组件
- 前端
【免费下载链接】varlet
A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.
相关推荐
Naive UI 无限滚动 Infinite Scroll 组件实战指南:滚动到底自动加载的实现原理与参数详解
Naive UI 无限滚动 Infinite Scroll 组件实战指南:滚动到底自动加载的实现原理与参数详解 导读 本文围绕 Naive UI 组件库中的 n
前端UI组件Naive UI 无限滚动组件 Infinite Scroll 实战指南:props 用法与源码原理
Naive UI 无限滚动组件 Infinite Scroll 实战指南:props 用法与源码原理 自 Naive UI 2.38.2 版本起,组件库内置了
前端UI组件javascript.info 无限滚动页面实战:用 getBoundingClientRect 与 scroll 事件实现触底自动加载
javascript.info 无限滚动页面实战:用 getBoundingClientRect 与 scroll 事件实现触底自动加载 无限滚动(endles
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考