虚拟列表在小程序中如何处理大量数据

手动滚动虚拟长列表实现

实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

计算当前可视区域起始数据索引(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 的数量,以及数据的传输量

这是数据初次家族只渲染首屏的数据

posted @ 2024-07-05 20:54  舒克无良  阅读(3)  评论(0编辑  收藏  举报