- 前端
- UI组件
- 移动开发
【免费下载链接】cube-ui
:large_orange_diamond: A fantastic mobile ui lib implement by Vue
cube-ui 的 RecycleList(1.12.0 起提供)是一款面向移动端的可回收滚动列表组件,它通过把 DOM 数量始终控制在一个很低的范围,让海量数据的列表渲染保持流畅。本文以官方文档为骨架,结合仓库源码与示例,完整讲解固定高度容器约束、默认"滚动到底部加载"与 Infinite 无限滚动两种交互、
onFetchPromise 数据协议、四个核心 props、四个插槽以及reset实例方法,并给出可直接运行的前后端(mock)示例代码。读完本文,你将能够在自己的 Vue 移动端项目中落地一个高性能、可扩展的"朋友圈式"长列表。
RecycleList 是 cube-ui 在 1.12.0 版本中新增的高性能列表组件,核心思路是虚拟化渲染 + 增量拉取:无论后端有多少数据,页面中真正挂载的 DOM 始终只有可视区域附近的一小批 item,其余数据只以坐标形式存在内存里。本文基于官方文档 recycle-list.md、组件源码 recycle-list.vue、模块入口 index.js、官方示例 recycle-list-default.vue 与 recycle-list-tombstone.vue,以及单元测试 recycle-list.spec.js 展开讲解。
一、使用前的三项注意事项
文档在开篇就给出了三个决定组件能否正确运行的硬性约束,理解它们比写模板本身更重要。
1. 固定高度的父容器是无限滚动的前提
RecycleList 自身在源码中被固定为height: 100%(见 recycle-list.vue 的样式部分),滚动区域是组件内部自带overflow-y: auto的容器。因此必须有一个固定高度的父元素包裹组件,否则高度塌陷为 0,滚动与位置计算都无从谈起。文档中的做法是给外层view-wrapper使用position: fixed撑满视口:
/* 限制视口 包裹组件 */ .view-wrapper position: fixed top: 54px left: 0 bottom: 0 width: 100%官方示例 recycle-list-default.vue 采用了position: absolute+top/left/bottom的写法,效果等价;只要保证最终计算出的高度是确定值即可。
2. 不固定宽高的图片必须用样式占位
组件通过"预渲染测量高度"来定位每个 item(源码见下文"高度测量与定位"一节)。如果 item 里有图片,DOM 渲染完成时图片可能尚未加载,此时测量的高度是错的。因此调用方必须给未加载的图片固定一个占位尺寸。示例的做法是让头像元素拥有固定的48px × 48px尺寸与background-image兜底图:
.avatar border-radius: 50% min-width: 48px width: 48px height: 48px background-image: url('./unknown.jpg') background-size: cover3. 内部实现依赖 Promise,需要 polyfill
组件的mounted钩子里显式调用了checkPromiseCompatibility(),当window.Promise不存在时会输出警告信息'requires a Promise polyfill in this browser.'(常量PROMISE_ERROR,见 recycle-list.vue)。此外,加载流程中大量使用window.Promise.all等待每个 item 的高度测量完成,所以必须为不支持 Promise 的旧浏览器引入 polyfill。
二、默认模式:滚动到底部自动加载下一页
绝大多数列表交互是"用户滚到底部 → 请求下一页"。组件的默认实现正是围绕此交互设计的,官方完整示例见 recycle-list-default.vue。
1. 模板与数据协议
<div class="view-wrapper"> <cube-recycle-list class="list" :size="size" :on-fetch="onFetch" :offset="offset"> <template slot="item" slot-scope="{ data }"> <div :id="data.id" class="item" @click="handleClick(data)"> <div class="avatar" :style="{backgroundImage: 'url(' + (data.avatar || '') + ')'}"></div> <div class="bubble"> <p>{{ data.msg }}</p> <div class="meta"> <time class="posted-date">{{ data.time }}</time> </div> </div> </div> </template> </cube-recycle-list> </div>export default { data() { return { size: 50, offset: 100 } }, methods: { onFetch() { let items = [] return new Promise((resolve) => { // 模拟请求 50 条数据,因为 size 设置为 50 setTimeout(() => { for (let i = 0; i < 50; i++) { items.push({ id: i, avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/danpliego/128.jpg', msg: '123', time: 'Thu Oct 25 2018 15:02:12 GMT+0800 (中国标准时间)' }) } resolve(items) }, 1000) }) }, handleClick(data) { console.log('Item:' + data) } } }这里有几个必须遵守的约定:
onFetch是必传属性(源码中required: true),且必须返回一个 Promise;- 调用
resolve时必须把items(数组)传入,组件通过它拿到数据; - 如果想停止滚动,resolve 一个
false即可; - 若 resolve 的数组长度小于
size,组件也会自动判定"没有更多数据",停止继续拉取(见源码getItems中res.length < this.size的判断)。
2. 底层加载链路
组件加载逻辑在 recycle-list.vue 的getItems()方法中实现:
- 每次调用
onFetch()得到一个 Promise,push 进promiseStack,同时loadings数组记录一次"进行中"; - Promise resolve 后:若结果为
false则调用stopScroll停止;否则通过setList(index, res)把数据写入内部list(按index * size定位批次),随后loadItemsByIndex触发该批次 item 的高度测量与渲染; - 滚动时
_onScroll判断scrollTop + offsetHeight > heights - offset成立则再次调用load()拉取下一页; - 容器 resize 时
_onResize会把所有 item 标记为未加载并重新测量高度,避免布局变化导致错位。
在非 infinite 模式下,组件底部还会渲染一个加载提示区域(cube-recycle-list-loading),loading为真时可见,默认内容是cube-loading组件。
3. 通过作用域插槽拿到数据
item是带作用域的插槽,通过解构赋值slot-scope="{ data }"拿到 items 中的每一个数据项。注意示例中data可能是undefined(首批数据未到位时组件会先以空对象占位),因此模板里对data.avatar使用了(data.avatar || '')的兜底写法。
三、Infinite 模式:用 tombstone 占位符实现无限滚动
默认模式在等待数据时没有可见反馈。组件还提供了第二种交互——无限滚动模式:开启后,尚未加载数据的区域会先以tombstone(墓碑占位符)呈现,让用户感知到"这里即将有内容",交互更友好、人性化。官方完整示例见 recycle-list-tombstone.vue。
1. 模板结构
<div class="view-wrapper"> <cube-recycle-list class="list" :infinite="infinite" :size="size" :on-fetch="onFetch"> <!-- tombstone 的作用域插槽 slot-scope 必须声明 --> <template slot="tombstone" slot-scope="props"> <div class="item tombstone"> <div class="avatar"></div> <div class="bubble"> <p></p> <p></p> <p></p> <div class="meta"> <time class="posted-date"></time> </div> </div> </div> </template> <template slot="item" slot-scope="{ data }"> <div :id="data.id" class="item" @click="handleClick(data)"> <div class="avatar" :style="{backgroundImage: 'url(' + (data.avatar || '') + ')'}"></div> <div class="bubble"> <p>{{ data.msg }}</p> <div class="meta"> <time class="posted-date">{{ data.time }}</time> </div> </div> </div> </template> <!-- <div slot="spinner">Loading Data</div> --> <!-- <div slot="noMore">没有更多数据了</div> --> </cube-recycle-list> </div>注意:开启 infinite 后,tombstone作用域插槽是必须实现的,它是无限滚动模式下数据未到位时的占位视图。
2. Infinite 模式的实现原理
从源码看,infinite 模式与默认模式最大的差异在load()方法:
- 默认模式只有在
!this.loading(无进行中的请求)时才拉取数据; - infinite 模式会先把内部
items数组的容量一次性扩充size个槽位(items.length += this.size),每个空槽位通过setItem生成isTombstone: true的占位项,随后再getItems()异步填充真实数据。
模板层面,每个可视 item 中同时渲染了tombstone与item两个插槽,通过opacity与 500ms 的cube-recycle-list-transition过渡动画完成"占位 → 真实内容"的淡入切换:
<div v-if="infinite" :style="{opacity: +!item.loaded}"> <slot name="tombstone"></slot> </div> <div :style="{opacity: item.loaded}"> <slot name="item" :data="item.data"></slot> </div>数据加载完成后,removeUnusedTombs会从第一个仍然为 tombstone 的槽位处截断 items,回收多余占位。需要强调:无限滚动模式的自动停止条件与默认模式一致——onFetchresolvefalse,或返回数组长度小于size,二者都会触发stopScroll,并将noMore置为true。
3. spinner 与 noMore 插槽
spinner:加载更多时的提示视图,默认是一个 loading 图(cube-loading);noMore:无更多数据时的提示视图,默认不渲染内容。官方示例中自定义为<div slot="noMore">没有更多数据了</div>(见 recycle-list-tombstone.vue)。
四、Props 配置一览
官方文档给出了完整的 props 表格:
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | - | - | - | - | - | | infinite | 是否无限滚动模式 | Boolean | - | false | | size | 一次加载的数据量 | Number | - | 20 | | offset | 底部拉取更多数据的距离 | Number | - | 200 | | onFetch | 获取更多数据 | Function | 必传 | - |
与源码 recycle-list.vue 中 props 定义对照后有两处细节值得留意:
offset默认值的差异:文档表格写的是 200,而当前仓库源码中为default: 100。两者语义一致——"距底部还剩多少像素时触发下一次拉取";实际使用时建议显式传入以消除歧义,示例中即显式传了:offset="100"。onFetch的数据协议:resolve 的第一个参数必须是数组或false。如果是数组且长度小于size,组件会停止无限滚动;如果是false,同样停止。该约定在源码getItems中均有对应分支(!res→stopScroll;res.length < this.size→stopScroll)。
TypeScript 类型声明 RecycleList.d.ts 中对应的签名是onFetch: Promise<any[] | false>,reset: () => void,与文档描述完全一致。
五、插槽与实例方法
插槽
| 名字 | 说明 | 参数 | | - | - | - | | tombstone | 实现个性化tombstone样式的作用域插槽 | - | | item | 列表某项的作用域插槽,通过此插槽实现想要的渲染视图 | data: 调用方传入的数据项集合的某一项 | | spinner | 加载更多的提示文案的具名插槽 | - | | noMore | 无更多数据的提示文案的具名插槽 | - |
使用注意:infinite 模式下tombstone的slot-scope必须声明(官方示例中声明为slot-scope="props");item是唯一带数据参数的作用域插槽,通过slot-scope="{ data }"解构。
实例方法:reset
reset用于清空列表全部内容、重置数据。源码实现中它会将items、heights、startIndex、loadings、noMore、list、promiseStack七个内部状态全部重置,并把容器scrollTop归零,随后重新执行load()拉取首批数据。典型场景是"下拉刷新"或切换数据源后重新开始。
六、虚拟化渲染原理:从源码看"低 DOM 数量"如何实现
文档强调组件"始终将 DOM 的数量控制在一个很低的范围",其关键在于源码中的三个机制:
- 可视窗口切片(visibleItems):
visibleItems计算属性只渲染[startIndex - size, startIndex + size]区间内的 item,即最多2 * size个真实 DOM。滚动时_onScroll调用updateStartIndex,根据scrollTop与每个 item 的top坐标二分定位新的起始索引,实现窗口的滑动更新。 - 绝对定位 + translate 布局:每个 item 都是
position: absolute,通过transform: translate(0, top)摆放到正确位置,外层cube-recycle-list-items的高度由heights(所有 item 高度累加)撑起,从而制造出"整份列表"的滚动条。 - 预渲染高度池(preloads pool):模板底部的
cube-recycle-list-pool区域隐藏渲染所有未测量高度的 item,updateItemHeight在$nextTick后读取offsetHeight存入height,随后updateItemTop依次累加计算每个 item 的top与总高度;测量完成的 item 会从预渲染池移除。这也是前文"图片必须固定宽高"的原因——测量发生在图片加载完成之前,不固定尺寸必然导致offsetHeight失真。
单元测试 recycle-list.spec.js 从行为层面对这些机制做了验证:render correct count data用例在滚动后断言items.length从 10 增长到 15(滚动触发增量拉取);should stop scroll用例断言 resolvefalse后noMore变为true;should clean all contents用例验证reset()后list.length归零并重新加载。这些用例可直接作为你理解组件行为边界的参考。
七、快速接入步骤
- 安装:在项目中安装 cube-ui(当前仓库为组件库源码,可直接参考 package.json 中的依赖与构建配置),并按常规方式引入组件库;
- 注册:RecycleList 的模块入口 index.js 通过
Vue.component(RecycleList.name, RecycleList)完成全局注册,注册后即可在模板中使用<cube-recycle-list>; - 包一层固定高度的容器:如本文第一部分所示,用
position: fixed/absolute或显式高度限定滚动区域; - 实现
onFetch:返回一个 Promise,resolve 数组或false,确保旧环境有 Promise polyfill; - 编写插槽:至少实现
item作用域插槽;infinite 模式下补全tombstone插槽,按需自定义spinner与noMore; - 处理图片:对列表项中的图片预先固定占位尺寸;
- 需要重置时调用
reset(),组件会清空数据并重新从第一批开始加载。
按此流程,即可在移动端实现一条"数据无限、DOM 有限"的高性能长列表。
- 前端
- UI组件
- 移动开发
【免费下载链接】cube-ui
:large_orange_diamond: A fantastic mobile ui lib implement by Vue
相关推荐
BetterScroll Infinity 插件:海量列表数据下的无限滚动渲染方案
BetterScroll Infinity 插件:海量列表数据下的无限滚动渲染方案 infinity 插件为 BetterScroll 提供了无限滚动(unli
前端UI组件BetterScroll infinity 插件完全指南:海量列表数据下的无限滚动渲染方案
BetterScroll infinity 插件完全指南:海量列表数据下的无限滚动渲染方案 导读 @better scroll/infinity 是 Bette
前端UI组件10万条数据丝滑滚动:BetterScroll无限列表渲染实战指南
10万条数据丝滑滚动:BetterScroll无限列表渲染实战指南 你是否遇到过移动端列表滑动卡顿、白屏崩溃?当数据超过1万条时,传统渲染方式往往让页面如同"老
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考