抓紧时间学习,然后睡觉 😪
前言
在工作中有时候会遇到类似这样的业务需求:无限加载列表数据、不能使用分页来加载列表数据,我们把这种列表统称为长列表
。
为什么要使用虚拟列表
假设页面上有 10000 条数据,不论是一次性渲染还是进行滚动浏览,都会存在白屏或者卡顿的情况,特别是在一些配置比较低的机型会更加明显。下面是完子说
首页瀑布流
列表的滚动录屏。
可以明显看出在快速滚动的时候会出现白屏,甚至是卡顿的情况。这是因为当列表数据很大时,页面渲染的节点过多,而且这些节点里面又包含子节点,这样将会消耗巨大的性能,在小程序上还有可能因为内存不足而造成闪退。
虚拟列表
就是这类问题的一个解决方案。
什么是虚拟列表
简单来说虚拟列表
其实就是一种按需显示的实现。具体的做法是:只对可视区域
进行渲染,对于非可视区域
部分渲染或者不渲染,从而达到极高的渲染性能。
假设页面需要展示 1000 条数据,页面的可视区域
的高度为500px
,列表项的高度为50px
,因此在页面的可视区域
范围内最多只能显示 10 条数据,所以在首次渲染的时候,我们只需要渲染 10 条数据即可。
接下来我们分析下滚动发生时,可以通过计算当前的滚动值,获得此时在页面的可视区域
应该渲染的列表项。
假如滚动发生,并且滚动值是100px
,那么可以计算出在可视区域
内的列表项为第3项
至第12项
。
瀑布流列表的变形
回归到我的项目,完子说
首页是以瀑布流
的形式来展示列表的,所以结构会跟上面的列表有所不同。下面是瀑布流
列表的基本结构。
由于瀑布流
列表结构的特殊性,所以没办法沿用上面那种方式,来换一种实现思路。
一页请求 10 条数据,一条数据认为是一个文章卡片
,把文章卡片
按左右两列哪边的高度较小就优先插哪边(所以这种方案只适合已知数据高度的情况下),并且认为一页的数据就是一屏
,那么就可以得到下面的这种结构。
由于每个文章卡片
的高度都是不一致的,这样就呈现出错落有致的瀑布流
效果。但是这里每一屏
是分隔开的,所以会存在下面这种情况:
要解决这种情况,只需要计算出前一屏
的左右列的高度差
,便可以得出下一屏
的偏移量
。
实现原理
瀑布流
虚拟列表的实现,实际上就是在首屏加载时,只渲染可视区域
内需要的列表项,当页面滚动时,判断目标节点的内容是否进入可视区域
内,如果进入了就把内容渲染出来。
这里可以使用IntersectionObserver
对象,来判断目标节点是否已进入可视区域
。
// index.wxml
<wxs module="filter">
var isInVisiblePages = function (visibleIndexs, current) {
return visibleIndexs.indexOf(current) > -1
}
var offsetTop = function (offset) {
return offset > 0
? 'top: -' + offset + 'px'
: ''
}
module.exports = {
isInVisiblePages: isInVisiblePages,
offsetTop: offsetTop
}
</wxs>
<view class="waterfull">
<view class="waterfull__item" wx:for="{{ records }}" wx:key="index" style="height: {{ item.height }}px" data-index="{{ index }}">
<block wx:if="{{ filter.isInVisiblePages(visibleIndexs, index) }}">
<view class="waterfull__item__left" style="{{ filter.offsetTop(item.leftOffset) }}">
<template is="article" data="{{ data: item.leftData }}"></template>
</view>
<view class="waterfull__item__right" style="{{ filter.offsetTop(item.rightOffset) }}">
<template is="article" data="{{ data: item.rightData }}"></template>
</view>
</block>
</view>
</view>
<template name="article">
<view class="article-box" wx:for="{{ data }}" wx:for-item="article" wx:for-index="articleIndex" wx:key="articleIndex">
<view class="article">
<view class="article__poster">
<image class="article__poster__img" src="{{ article.coverImage }}" style="height: {{ article.realHeight }}rpx" mode="aspectFill" />
</view>
<view class="article__content">
<view class="article__content__title">{{ article.title }}</view>
<view class="article__content__creator">
<image class="article__content__creator__avatar" src="{{ article.creator.avatar }}" mode="aspectFill" />
<view class="article__content__creator__nickname">{{ article.creator.nickname }}</view>
</view>
<view class="article__content__topic" wx:if="{{ article.topic }}">
<image class="article__content__topic__icon" src="https://pub-img.perfectdiary.com/material/image/2021/05/5d604f0b034d4c7f9b857fb0919f3ee3.png" />
<view class="article__content__topic__title">{{ article.topic.title }}</view>
</view>
</view>
</view>
</view>
</template>
// index.wxss
.waterfull {
padding: 0 12rpx;
background: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);
}
.waterfull__item {
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.waterfull__item__left, .waterfull__item__right {
position: absolute;
top: 0;
width: calc(50% - 5rpx);
}
.waterfull__item__left {
left: 0;
}
.waterfull__item__right {
right: 0;
}
.article-box {
padding: 6rpx 0;
}
.article {
font-size: 0;
box-shadow: 0px 4rpx 16rpx 0px rgba(34, 34, 34, 0.05);
}
...........
// index.js
const App = getApp()
let rtp = 0.5
let maxHeight = 238.7
let maxWidth = 179
const coverImgProportion = 0.75 // 封面图宽高比例
const proportion = 0.477 // 瀑布流 封面图高与屏幕的比例
if (App.systemInfo && App.systemInfo.windowWidth) {
rtp = App.systemInfo.windowWidth / 750
maxWidth = App.systemInfo.windowWidth * proportion || maxWidth
maxHeight = (App.systemInfo.windowWidth * proportion) / coverImgProportion || maxHeight
maxHeight += 1
}
Component({
// 组件的属性列表
properties: {
articles: {
type: Array,
value: [],
observer (list) {
this.handleArticleData(list)
}
}
},
// 组件的初始数据
data: {
records: [], // 总列表
visibleIndexs: [], // 可渲染的索引列表
},
lifetimes: {
detached () {
this.disconnect()
}
},
pageLifetimes: {
show () {
this.reconnect()
}
},
ready () {
this.createObserve()
},
// 组件的方法列表
methods: {
// 处理列表数据
handleArticleData (list) {
// 拆分成分屏数组 一屏 10 个
const _list = [...list]
const allList = []
while (_list.length) {
const currentList = _list.splice(0, 10)
allList.push({
data: currentList
})
}
this.handleWaterfullList(allList)
},
handleWaterfullList (list) {
// 单位均为 rpx
const titleHeight = 88 // 标题高度
const avatarHeight = 34 // 头像高度
const avatarMarginTop = 12 // 头像上边距
const topicHeight = 48 // 话题高度
const topicMarginTop = 12 // 话题上边距
const contentPaddingTop = 12 // 内容上边距
const contentPaddingBottom = 16 // 内容下边距
const boxPaddingTop = 6 // 盒子上边距
const boxPaddingBottom = 6 // 盒子下边距
// 固定的高度集合
const fixedHeight = [
titleHeight, avatarHeight, avatarMarginTop, contentPaddingTop,
contentPaddingBottom, boxPaddingTop, boxPaddingBottom
]
list.forEach((item, index) => {
const isLast = index + 1 === list.length
// 这里的高度要先减去偏移量
let leftHeight = 0 - item.leftOffset || 0
let rightHeight = 0 - item.rightOffset || 0
const leftData = []
const rightData = []
item.data.forEach(article => {
article.realHeight = this.calcImageHeight(article)
const heights = [...fixedHeight, article.realHeight]
if (article.topic) {
heights.push(topicHeight, topicMarginTop)
}
// 计算卡片高度
// 由于存在误差,所以将每个高度转换成 px 再相加
const cardHeight = heights.reduce((total, current) => total + this.handleRtoP(current), 0)
article.cardHeight = cardHeight
// 计算左右两列的高度
// 保证左右两列的高度差不会太大
if (leftHeight <= rightHeight) {
leftHeight += cardHeight
leftData.push(article)
} else {
rightHeight += cardHeight
rightData.push(article)
}
})
// 计算偏移量
if (!isLast) {
const offset = Math.abs(leftHeight - rightHeight)
const nextIndex = index + 1
if (leftHeight >= rightHeight) {
list[nextIndex].rightOffset = offset
list[nextIndex].leftOffset = 0
} else {
list[nextIndex].leftOffset = offset
list[nextIndex].rightOffset = 0
}
}
item.height = Math.max(leftHeight, rightHeight)
item.leftData = leftData
item.rightData = rightData
})
this.setData({
records: list
}, () => {
this.reconnect()
})
},
calcImageHeight (article) {
// 根据封面原始大小和比例换算成对应的尺寸
// 超过限制则采用最大限制的高度
let imageHeight = maxHeight
if (article.imgHeight && article.imgWidth) {
imageHeight = (maxWidth * article.imgHeight) / article.imgWidth
}
if (imageHeight > maxHeight) {
imageHeight = maxHeight
}
// 先转换成 rpx
imageHeight = imageHeight / rtp
return imageHeight
},
handleRtoP (height) {
return parseInt(height * rtp)
},
// 创建可视区域监听
createObserve () {
if (this.ob) return
this.ob = this.createIntersectionObserver({
observeAll: true,
initialRatio: 0,
}).relativeToViewport({
bottom: 0
})
this.ob.observe('.waterfull__item', res => {
const { index } = res.dataset
if (res.intersectionRatio > 0) {
this.setData({
visibleIndexs: [index - 1, index, index + 1]
})
}
})
},
connect () {
this.createObserve()
},
// 重连可视监听
reconnect () {
if (!this.ob) return
this.disconnect()
this.connect()
},
// 断开可视监听
disconnect () {
if (!this.ob) return
this.ob.disconnect()
this.ob = null
},
}
})
最终效果如下:
线上优化后的效果如下:
参考
今天的文章📚 瀑布流的虚拟列表分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21354.html