当前位置: 首页 > Web前端 > vue.js

哈啰商城H5中虚拟列表的实践

时间:2023-03-31 16:25:12 vue.js

为什么要使用虚拟列表在哈啰商城中,有很多很长的列表数据,比如下图列出的产品瀑布和销售地点列表。当用户滑动到页面底部时,会加载新的数据,页面上的DOM节点会越来越多,容易造成页面卡顿,交互不流畅。对于这种长列表场景,我们可以使用虚拟列表进行优化。什么是虚拟列表虚拟列表,顾名思义,不是真实数据列表的反映,只是截取一部分列表数据,填充可见区域。以品牌会场页面为例,在屏幕可显示范围内可能只有3条卡片数据,但在屏幕可视范围外,我们可能刷卡请求几十万条数据,这些data是我们看不到的,但是它实际存在于页面DOM中。我们完全可以只渲染屏幕中间可以看到的几张卡片。当用户滚动时,根据滚动距离计算并替换这些卡片中的数据,达到模拟真实列表滚动的效果。如何实现虚拟列表先写一个简单的demo,模拟页面下拉加载数据的场景。每个页面加载20条数据,下拉到底部继续加载20条数据。我们可以在开发者工具中看到,DOM节点在不断增加。现在让我们一步步实现一个虚拟列表。假设我们滚动可视区域的高度为1000px,每个列表项的高度为100px,那么可视区域可以渲染10条数据。当滚动距离为0时,渲染出来的列表项的数据索引是0到9(因为我们用数组的slice方法切数组时,索引是左闭右开区间,所以这里的数据切割是slice(0,10))。滚动100px后,相当于向上滚动了第一个列表项,然后可以将实际渲染的列表项数据从1替换为10(slice(0,11))。同理,滚动scrollTop距离后,相当于向上滚动了之前的(scrollTop/itemHeight)个列表项,实际渲染的列表项数据为(scrollTop/itemHeight)到(scrollTop/itemHeight)+9。当然,如果滚动的scrollTop小于一个列表项高度,则当前列表项还在可见区域,不能被替换,所以我们在使用scrollTop/itemHeight时,需要向下取整。定义如下变量:滚动区域的高度记录为scrollContainerHeight每个列表项的高度是固定的,记录为itemHeight列表可见范围内的列表项数量,记录为visibleCount滚动距离记录为scrollTop完整的列表数据记录为listData实际渲染的列表数据记录为visibleList列表项的起始索引,startIndex列表项的结束索引,endIndex列表的向下偏移量,以及scrollOffset获取的下面的计算公式:visibleCount=Math.ceil(scrollContainerHeight/itemHeight)startIndex=Math.floor((scrollTop/itemHeight))endIndex=startIndex+visibleCountvisibleList=listData.slice(startIndex,endIndex)scrollOffset=startIndex*itemHeight让我们用代码来实现它:consttotalHeight=computed(()=>{returnlistData.value.length*itemHeight.value;})constscrollEvent=(e)=>{constscrollTop=e.target.scrollTop;//获取滚动距离startIndex.value=Math.floor(scrollTop/itemHeight.value);//起始索引是单个列表项的滚动距离/高度endIndex.value=startIndex.value+visibleCount.value;//结束索引为起始索引+可见区域的列表项数visibleList.value=listData.value.slice(startIndex.value,endIndex.value);scrollOffset.value=startIndex.value*itemHeight.value;//偏移量为已经滑出的列表项数*单个列表项的高度}看目前的实现效果,无论列表项有多少,永远只有10个可变DOM高度的虚拟列表在页面上呈现的场景,例如商品瀑布流。由于列表项的高度不同,如果仍然用上面的方法计算索引替换数据,会不准确,页面会抖动。我们可以先设置一个预估高度,当列表项加载时,获取列表项的实际渲染高度并更新。列表中每一项的预估高度为50px,然后我们得到初始位置数组,其中index为列表项数据的实际索引,height为列表卡片的高度,top为卡片到卡片的距离top,bottom是卡片底边的位置。当页面呈现时,我们得到每个项目的实际高度。如果实际高度与之前预估的高度不一致,则更新item的高度值。例如:索引为0的列表项实际高度为44px,更新高度为44px。由于高度小了6px,底边的位置也向上6px,所以bottom更新为50-6=44。假设索引为1的列表项的实际高度是我们估计的50,但是由于索引为0的列表项的高度降低了,因此索引为1的列表项的顶部和底部也需要相应地减少6,所以我们发现,当某个item的高度发生变化时,这个item之后的所有列表项的top和bottom都会受到影响,我们要做一个数据更新。constupdateItemHeight=()=>{constnodes=visibleItemRef.value;nodes.forEach((node)=>{if(!node){return;}constrect=node.getBoundingClientRect();constid=node.id;constoldHeight=positions.value[id].height;//获取当前渲染的列表项之前的高度constcurrentHeight=rect.height;//获取当前渲染的列表项的当前高度constdiffHeight=oldHeight-currentHeight;//获取两个高度的差值if(diffHeight!==0){positions.value[id].height=currentHeight;//更新此项的高度positions.value[id].bottom=positions.value[id].bottom-diffHeight;}});conststartId=+nodes[0].id;//当前渲染列表项的第一项的实际索引//由于当前索引的高度发生变化,因此从当前索引开始的所有项top和bottom都必须更新for(leti=startId+1;iscrollTop的卡片,记录为startIndex,修改偏移量为startIndex-1的卡片底部值。constscrollEvent=(e)=>{constscrollTop=e.target.scrollTop;//获取滚动距离conststartItem=positions.value.find((p)=>{returnp.bottom>scrollTop;});if(startItem){startIndex.value=startItem.index;//起始索引是第一个大于scrollTop的底部值}else{startIndex.value=0;}endIndex.value=startIndex.value+visibleCount.value;//end索引为起始索引+可见区域的列表项数visibleList.value=listData.value.slice(startIndex.value,endIndex.value);如果(startIndex.value>0){scrollOffset.value=positions.value[startIndex.value-1].bottom;//偏移量为已经滑出的列表项的底边位置}else{scrollOffset.value=0;}};以上所有代码实现只是从最基本的思路SimpleDemo实现,在实际实现过程中,我们还可以对虚拟列表的代码做很多优化。比如结合分页请求、设置缓冲区、计算偏移量,使用二分查找减少查找次数和滚动节流。当然,业界已经有很多封装好的库可以直接使用,比如vue-virtual-scroller。商场H5的实际效果是在商场H5的品牌特卖会场。在引入vue-virtual-scroller的基础上,我们增加了下拉分页请求。效果如下:(本文作者:马欣欣)本文由哈啰技术团队制作。未经许可不得商业复制或使用。如非商业目的转载或使用本文内容,请注明“内容转载自你好技术团队”。