上拉加载 VS 虚拟列表? uni-app长列表性能优化实战
上拉加载 or 虚拟列表
在项目开发中经常会遇到渲染后端返回1k+以上数据的长列表
的业务需求,大多数情况下首选方案是通过后端返回分页字段进行分页加载
优化:
如下图实现网易☁️歌单列表无限加载:
然而某些情况下,后端必须一次性返回全部数据,如果将这些数据同时渲染到页面,会出现非常明显的等待时间
⌛️,数据量越大等待时间就越长,显然对于用户体验非常不友好,那么这种情况可以考虑通过虚拟列表
来进行列表优化:
这是一个1000+首歌左右
的返回每首歌全部数据的网易云歌单数据列表,小带宽服务器光是返回接口全部数据就花了12s左右????,如果再等待全部图片加载完毕会造成更长的等待时间和大量的流量消耗????
遇到这种情况作为前端首先要和后端进行沟通,比如本接口的Size高达1.7MB,其中返回的大多数字段在数据列表渲染
中其实都是非必要字段,优化冗余字段
并进行数据压缩
可以大幅度加快接口响应速度⚡️⚡️⚡️
接下来考虑数据渲染问题,显然一次性将所有数据同时渲染显示是不切实际的,那么思考下????,将数据进行切割后根据页面滚动高度分批进行渲染
,每次只加载可视区域内的数据
是否可以加快页面加载过程?
显然这个方案是可行的✌️,这就是接下来虚拟列表的实现思路????
下图为虚拟列表+图片懒加载
优化后的1000+长列表,获取到接口数据的同时可视区内数据就已经渲染完毕,快速滑动加载新数据也不会出现明显加载延迟:
实现一个简单的虚拟列表
⚠️本文实现的虚拟列表能够正常运行的前提是
列表项高度相同
思路????
获取起始和结束索引,起始索引
Math.floor(scrollTop / itemHeight)
,结束索引startIdx + showNum
从原生数据中截取可视区域数据
list.slice(startIdx, endIdx)
计算偏移量
offset = scrollTop - (scrollTop % itemHeight)
监听
scroll
事件,获取到scrollTop
并实时计算可视区域高度注意可视区域高度应
略大于
列表组件高度,撑出滚动条,但不应设置过大造成加载数据过多
实现✍️
demo中通过
document.getElementsByClassName
获取scrollTop
uni-app项目应使用e.detail.scrollTop
获取scrollTop
这里实现的虚拟列表是列表项固定高度,想要实现动态高度列表项可以参考这篇文章???? 「前端进阶」高性能渲染十万条数据(虚拟列表)
优化(骨架屏)
虽然虚拟列表可以优化页面渲染速度,但接口一次性返回大量数据难免会出现较长等待时间,可以考虑使用骨架屏
优化用户等待体验:
实现上拉加载
除了特殊情况外,大多数情况下长列表优化首选上拉加载
,根据后端返回的分页字段
对数据进行分页处理
如下图根据more
字段判断是否请求下一页
思路????
一般实现分页逻辑需要定义一个pageData
对象,记录页面分页数据配置项????,每次发送请求时传递pageData对应信息
// PageData大致数据类型,可根据需求拓展 interface PageData { init: boolean, // 初始化 loading: boolean, // loading more: boolean, // 是否有下一页 limit: number, // 单次分页返回数据条数 before?: number, // 分页参数,取list最后一条数据updateTime page?: number, // 页数 ... } 复制代码
判断是否已经是
最后一页
?防止重复请求空数据判断当前是否正处于
loading
状态,防止频繁请求接口请求接口返回分页数据
判断是否为
第一页
?生成或更新数据列表
list
更新
pageData
状态根据返回分页字段判断是否有
下一页
接下来对页面滑动高度进行监听,uni-app通过onReachBottom
监听上拉触底事件,触底更新分页数据
实现✍️
uni-app(伪代码)
import { ref, reactive } from 'vue'; interface List { id: number, name: string, picUrl: string, ... } interface PageData { init: boolean, loading: boolean, more: boolean, limit: number, before: number, } const list = ref<List[]>([]); const pageData = reactive<PageData>({ // 分页数据 init: false, loading: false, more: true, limit: 10, before: 0, }) const getData = async () => { // 获取列表数据 if (pageData.more === false) { uni.showToast({ icon: "none", title: "没有更多了" }) return } else if (pageData.loading === true && list.value) { uni.showToast({ icon: "none", title: "请勿频繁触发加载" }) return } else { pageData.loading = true; const { list, lasttime, more } = await getDataList({ limit: pageData.limit, before: pageData.before ... }) if (pageData.before <= 0) { list.value = list; } else { list.value.push(...list); } pageData.init = true; pageData.loading = false; pageData.before = lasttime; pageData.more = more; } } ... const onReachBottom = throttle(() => { // 上拉触底 节流防止频繁调用接口 // console.log('到底了') getPlayListData() }, 1000) getPlayListData() 复制代码
优化(Hooks)
由于上拉加载列表
是一个移动端非常常见的需求,几乎每个列表组件都会用到,每写一个列表都要复制一遍这段代码,因此抽离涉及上拉加载的相关逻辑便可以节省很多重复代码。下面通过Vue3
的Hooks
对相关逻辑进行抽离复用
首先考虑需要暴露出来的参数,比较重要的有4个:
listReq
必传 HTTP异步请求函数listStr
必传 接口返回的列表字段并不总是res.list
,进行动态字段匹配pageData
必传 分页数据配置项data
可选 额外传入参数{key: value}
形式
// 列表加载Hooks useList import { ref } from 'vue'; interface PageData { init: boolean, loading: boolean, more: boolean, limit: number, before?: number, page?: number, } const useList = (listReq: Function, listStr: string, pageData: PageData, data?:Object):any => { const list = ref<any>([]); if(!listReq) { return new Error('请传入接口调用方法!') }else if(!listStr){ return new Error('请传入接口返回列表字段!') }else if(!pageData) { return new Error('请传入分页数据配置项!') } if(data && Object.prototype.toString.call(data) !== '[object Object]') { return new Error('额外参数请以对象形式传入') } const params = {...data} // 获取携带参数 const getData = () => { if (pageData.more === false) { uni.showToast({ icon: "none", title: "没有更多了" }) return } else if (pageData.loading === true && list.value) { uni.showToast({ icon: "none", title: "请勿频繁触发加载" }) return }else{ pageData.loading = true; listReq(params).then((res:any) => { const lasttime = res.lasttime const more = res.more // 判断是否是第一页 if (pageData.before! <= 0 || pageData.page === 1) { list.value = res[listStr]; } else { list.value.push(...res[listStr]); } pageData.init = true; pageData.loading = false; pageData.before = lasttime; pageData.more = more; }) } } getData() // 初始化获取接口数据 return { list, getData } } export default useList 复制代码
使用(伪代码)
import { ref, reactive, computed } from 'vue'; interface PageData { init: boolean, loading: boolean, more: boolean, limit: number, page: number, } const id = ref<number>(0) // ID const pageData = reactive<PageData>({ init: false, loading: false, more: true, limit: 10, page: 1, }) const offset = computed(() => { // offset return (pageData.page - 1) * pageData.limit }) const getListData = async(params:{id: number, offset: number}) => { try{ const { list, more } = await getUserList( params.id, pageData.limit, params.offset ) return { list, more } }catch(e){ console.log(e) } } const { list } = useList(getUserList, 'useList', pageData, {offset: offset.value, id: id.value}) 复制代码
动态获取列表高度
实际开发中,列表组件通常不是单独使用而是结合其他组件一起使用:
为了美观列表组件高度
应该恰好等于屏幕剩余高度
,具体实现可以参考之前写的这篇文章????:
uni-app 微信小程序通过Vue3 Hooks 实现动态填充页面剩余高度
总结
完整的使用场景和源码
接口数据一次性返回 ➡️ 虚拟列表 + 图片懒加载 + 骨架屏
接口数据分页返回 ➡️ 上拉加载 + Hooks
作者:CalDey
链接:https://juejin.cn/post/7170559014604898334