☰
Varlet List 组件详解:无限滚动触底加载的用法与源码级原理
2026/10/13 12:21:17 网站建设 项目流程
  • UI组件
  • 前端

【免费下载链接】varlet

A Vue3 component library based on Material Design 2 and 3, supporting mobile and desktop.

项目地址:https://gitcode.com/gh_mirrors/va/varlet
点击查看免费下载

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() }
  1. 滚动监听:滚动容器每次scroll事件都会调用check(无节流,依赖守卫条件短路);
  2. 初始化检查:immediate-check默认true,挂载后立即check一次——若首屏内容不足一屏(初始即触底),会立刻触发首次加载,避免用户"拉不动";
  3. 状态变化重检: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加载状态booleanfalse
v-model:error错误状态booleanfalse
immediate-check初始化时是否立即进行位置检测booleantrue
finished是否加载完成booleanfalse
offset距底部触发距离string | number0
loading-text加载中文案stringLoading
finished-text加载完成文案stringNo more
error-text加载失败文案stringLoad 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-height50px
--list-finished-height50px
--list-error-height50px
--list-loading-color#888
--list-finished-color#888
--list-error-color#888
--list-loading-font-sizevar(--font-size-md)
--list-finished-font-sizevar(--font-size-md)
--list-error-font-sizevar(--font-size-md)

例如将加载区调矮并换色:

:root { --list-loading-height: 40px; --list-loading-color: var(--color-primary); }

测试用例对行为的佐证

单元测试tests/index.spec.js 从三个层面验证了上述机制:

  • 触底触发:mockgetBoundingClientRect返回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.

项目地址:https://gitcode.com/gh_mirrors/va/varlet
点击查看免费下载
上一篇:OpenNI API完全手册:开发者必备的函数与接口速查指南
下一篇:JS The Right Way依赖管理:npm与yarn的最佳实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询