虚拟列表在小程序中如何处理大量数据
手动滚动虚拟长列表实现
实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
计算当前可视区域起始数据索引(startIndex)
计算当前可视区域结束数据索引(endIndex)
计算当前可视区域的数据,并渲染到页面中
计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上
0x1 实现一个“定高”虚拟列表https://codesandbox.io/s/A-better-v-list-bkw1t?file=/src/App.js
不定高参考:
https://codesandbox.io/s/a-v-list-has-dynamic-inner-height-forked-tsftlj?file=/src/VList.tsx
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=750, user-scalable=no, target-densitydpi=device-dpi">
<script src="pubilc/js/mobileRem.js"></script>
<link rel="stylesheet" type="text/css" href="pubilc/css/style.css" />
<script src="pubilc/js/jquery.js"></script>
<title></title>
</head>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
#app {
width: 7.5rem;
height: 667px;
background-color: antiquewhite;
}
.content {
height: 5rem;
/* overflow-y: auto; */
overflow: hidden;
}
.item {
text-align: center;
}
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
left: 0;
bottom: 0;
right: 0;
z-index:1;
}
.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.infinite-list-item{
height:60px;
line-height: 60px;
}
</style>
<body>
<div id="app">
<div class="content">
<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
</div>
</div>
</div>
</div>
</body>
</html>
<script>
$(function() {
var content = document.querySelector(".content")
const windowWidth = document.documentElement.clientWidth
const windowHeight = document.documentElement.clientHeight
const itemSize = 60; //每一项的高度
const listData = [];
let visibleData=[]
const state = {
start: "",
end: "",
startOffset: "",
//可视区域高度
screenHeight:0,
visibleCount:0//可显示列表在视窗的数量
}
$(".content").css('height', `${windowHeight}px`)
$(".content").css('width', `${windowWidth}px`)
getListData();//获取模拟数据
const listHeight = listData.length * itemSize; //列表总高度
//$(".infinite-list-phantom").css('height', `${listHeight}px`)
$('.infinite-list-container').scroll(function(e) {
let scrollTop=$(this)[0].scrollTop
console.log($(this)[0].scrollTop)
//此时的开始索引
state.start = Math.floor(scrollTop /itemSize);//返回一个最大整数的数字索引
state.end=state.start + state.visibleCount
state.startOffset = scrollTop - (scrollTop % itemSize);
$('.infinite-list').css('transform',`translate3d(0,${state.startOffset}px,0)`)
getVisibleData()
})
function getListData(){
//数据区域
for (let i = 0; i < 100000; i++) {
listData.push({
id: i,
value: i
});
}
}
//获取每次可视区域能看到多少条数据
function getVisibleCount(){
state.visibleCount= Math.ceil(state.screenHeight / itemSize);//大于或等于给定数字的最小整数。
console.log(state)
}
//页面初始化渲染获取页面高度以及初始化索引计算位置
function getIndexNumber(){
state.screenHeight=document.documentElement.clientHeight
state.start=0;
state.end=state.start + Math.ceil(state.screenHeight / itemSize)
}
//渲染数据
function getVisibleData(){
visibleData=listData.slice(state.start,Math.min(state.end,listData.length)+1);//因为10条数据无法撑开容器,需要增加一条,如果item足够大,那么久不需要,或者增加元素空数据标签撑破容器,但是底部会留白
var linkItem
visibleData.forEach((item,index)=>{
if(linkItem){
linkItem+=`<div class="infinite-list-item">${item.id}</div>`; //创建一个节点
}else{
linkItem=`<div class="infinite-list-item">${item.id}</div>`; //创建一个节点
}
$(".infinite-list").html(linkItem); //将这个节点加入到table中
})
}
getIndexNumber();
getVisibleCount();
setTimeout(()=>{
getVisibleData();
},100)
})
</script>
无缝滚动的虚拟列表
屏幕可视为一个列表,第二个列表则为撑开容器,有足够的高度可以让列表1进行滑动
两个列表元素
普通的无缝滚动列表2用来拷贝一份列表1进行滚动
l2.innerHTML = l1.innerHTML;
<div class="infinite-list1">
</div>
<div class="infinite-list2">
</div>
计算列表数据高度
itemSize为假设的item元素高度
listData为数据总长度
需要为两个列表元素添加高度,有足够的高度撑开盒子,才能滚完第一个列表进行无缝
const itemSize = 60; //每一项的高度
const listHeight = listData.length * itemSize; //列表总高度
$(".infinite-list1").css('height', `${listHeight}px`)
$(".infinite-list2").css('height', `${listHeight}px`)
页面初始化渲染获取页面高度以及初始化索引计算位置
function getIndexNumber(){
state.start=0;
state.end=state.start + Math.ceil(state.screenHeight / itemSize)
}
获取每次可视区域能看到多少条数据
function getVisibleCount(){
state.visibleCount= Math.ceil(state.screenHeight / itemSize);//大于或等于给定数字的最小整数。
}
渲染数据
function getVisibleData(){
visibleData1=listData.slice(state.start,Math.min(state.end,listData.length)+1);//因为10条数据无法撑开容器,需要增加一条或者增加元素空数据标签撑破容器,但是底部会留白
var linkItem1
visibleData1.forEach((item,index)=>{
if(linkItem1){
linkItem1+=`<div class="infinite-list-item1">${item.id}</div>`; //创建一个节点
}else{
linkItem1=`<div class="infinite-list-item1">${item.id}</div>`; //创建一个节点
}
$(".infinite-list1").html(linkItem1);
})
}
当列表1offsetHeight大于父盒子的时候开始滚动,要先拷贝一份旧数据进行复用
function autoScroll(){
if (l1.offsetHeight > content.offsetHeight) {
listData2=listData
//l2.innerHTML = l1.innerHTML; //克隆list1的数据,使得list2和list1的数据一样(但是列表2 滚动到顶部,再次重置到列表1 会有明细的闪烁)
scrollMove = setInterval(scrollup, 30); //数值越大,滚动速度越慢
content.onmouseover = function() {
// clearInterval(scrollMove)
}
}
}
父盒子距离头部的距离大于列表1元素的offsetHeight
关键调整transform大小,只进行scrollTop自增会导致列表1偏移出屏幕
function scrollup(){
if(content.scrollTop >= l1.offsetHeight){
content.scrollTop=0
$('.infinite-list1').css('transform',`translate3d(0,0,0)`)
//每次滚动完一个列表可以进行分页请求
//if (page.pageNo >= totalPage) return
// page.pageNo++;
// getList();
}else{
content.scrollTop++;
state.start = Math.floor(content.scrollTop /itemSize);//返回一个最大整数的数字索引
state.end=state.start + state.visibleCount;
state.startOffset = content.scrollTop - (content.scrollTop % itemSize);
$('.infinite-list1').css('transform',`translate3d(0,${state.startOffset}px,0)`)
getVisibleData();
//当滚到最后一条数据的时候合并在listData(要提前复制一份数据)---------解决无缝滚动方案2(解决方案1闪烁的问题)
if(state.end==listData.length-1){
listData=listData.concat(listData2)
console.log('合并')
}
}
}
全部代码
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=750, user-scalable=no, target-densitydpi=device-dpi">
<script src="pubilc/js/mobileRem.js"></script>
<script src="pubilc/js/jquery.js"></script>
<title></title>
</head>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
#app {
width: 7.5rem;
}
.content {
width: 100%;
height: 2rem;
background-color: beige;
overflow-y: auto;
position: relative;
}
.infinite-list-container {
overflow: hidden;
}
.infinite-list-phantom {
position: absolute;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
.infinite-list1 {
height: 60px;
line-height: 60px;
}
.infinite-list2 {
height: 60px;
line-height: 60px;
}
.infinite-list-item1{
}
</style>
<body>
<body>
<div id="app">
<div class="content">
<div class="infinite-list-container">
<!-- <div class="infinite-list-phantom"></div> -->
<div class="infinite-list1">
</div>
<div class="infinite-list2">
</div>
</div>
</div>
</div>
</body>
</html>
</body>
</html>
<script>
/*
1.固定高度
*/
$(function() {
const speed = 30; //速度
var listData = []
var listData2 = []
var visibleData=[]
const itemSize = 60; //每一项的高度
var state = {
start: "",
end: "",
startOffset: "",
//可视区域高度
screenHeight:100,
visibleCount:0//可显示列表在视窗的数量
}
var content = document.querySelector(".content")
var boxChilder = document.querySelector(".infinite-list-container")
var l1 = document.querySelector(".infinite-list1");
var l2 = document.querySelector(".infinite-list2");
getListData()
const listHeight = listData.length * itemSize; //列表总高度
$(".infinite-list1").css('height', `${listHeight}px`)
$(".infinite-list2").css('height', `${listHeight}px`)
//开始滚动
function autoScroll(){
if (l1.offsetHeight > content.offsetHeight) {
//l2.innerHTML = l1.innerHTML; //克隆list1的数据,使得list2和list1的数据一样--无缝滚动方案1
listData2=listData
scrollMove = setInterval(scrollup, 30); //数值越大,滚动速度越慢
content.onmouseover = function() {
// clearInterval(scrollMove)
}
}
}
function scrollup(){
if(content.scrollTop >= l1.offsetHeight){
content.scrollTop=0
$('.infinite-list1').css('transform',`translate3d(0,0,0)`)
}else{
content.scrollTop++;
state.start = Math.floor(content.scrollTop /itemSize);//返回一个最大整数的数字索引
state.end=state.start + state.visibleCount;
state.startOffset = content.scrollTop - (content.scrollTop % itemSize);
$('.infinite-list1').css('transform',`translate3d(0,${state.startOffset}px,0)`)
getVisibleData();
//当滚到最后一条数据的时候合并在listData(要提前复制一份数据)---------解决无缝滚动方案2(解决方案1闪烁的问题)
if(state.end==listData.length-1){
listData=listData.concat(listData2)
console.log('合并')
}
}
}
//获取每次可视区域能看到多少条数据
function getVisibleCount(){
state.visibleCount= Math.ceil(state.screenHeight / itemSize);//大于或等于给定数字的最小整数。
}
//页面初始化渲染获取页面高度以及初始化索引计算位置
function getIndexNumber(){
state.start=0;
state.end=state.start + Math.ceil(state.screenHeight / itemSize)
}
//渲染数据
function getVisibleData(){
visibleData1=listData.slice(state.start,Math.min(state.end,listData.length)+1);//因为10条数据无法撑开容器,需要增加一条,如果item足够大,那么久不需要,或者增加元素空数据标签撑破容器,但是底部会留白
var linkItem1
visibleData1.forEach((item,index)=>{
if(linkItem1){
linkItem1+=`<div class="infinite-list-item1">${item.id}</div>`; //创建一个节点
}else{
linkItem1=`<div class="infinite-list-item1">${item.id}</div>`; //创建一个节点
}
$(".infinite-list1").html(linkItem1);
})
}
function getListData() {
//数据区域
for (let i = 0; i <6; i++) {
listData.push({
id: i+'asfasf',
value: i
});
}
console.log(listData)
}
getIndexNumber()
getVisibleCount()
getVisibleData();
setTimeout(() => {
autoScroll();
}, 1000)
})
</script>
业务场景:1.小程序实现一个类似于瑞幸的选购商品页面(这里只说右侧实现)
解决长列表的手段本身就是控制 item 的数量,原理就是当数据填充的时候,理论上数据是越来越多的,让视图上的 item 渲染,而不在视图范围内的数据不需要渲染,那就不去渲染,这样的好处有:
由于只渲染视图部分,那么非视图部分,不需要渲染,或者只放一个 skeleton 骨架元素展位就可以了,首先这大大减少了元素的数量,也减少了图片的数量,直接减少了应用占用的内存量,减少了白屏的情况发生。
由于 item 数量减少了,减少 diff 对比的数量,提升了对比的效率。
如果 item 里面还有 setData 的操作,那么有间接性减少 setData 的数量,以及数据的传输量
这是数据初次家族只渲染首屏的数据