首页>>前端>>JavaScript->虚拟滚动

虚拟滚动

时间:2023-11-30 本站 点击:0

一、前言

做前端开发,难以避免的要和无限滚动加载这类交互打交道,简单的滚动加载大家都知道,监听滚动行为和位置,然后发请求加载下一屏数据。在当前vue/react大行其道的时代,数据驱动视图更新,修改数据来新增dom节点到列表元素中就可以实现,而且这种行为在大部分产品或场景中都没多大问题,但当代随着社交媒体的流行,大量的视频、图片、文字等数据被用户消费,传统的拼dom元素的滚动方案在性能上就存在下次,海量的数据早就大量的元素节点产生,从而会导致页面滚动的卡顿,那么有什么好的方案让用户浏览海量数据?

二、谷歌LightHouse开发推荐

chrome影响页面性能的因素:

总共有超过1,500个节点。

具有大于32个节点的深度。

有一个超过60个子节点的父节点。

三、效果对比

内容社交消耗是目前网上用户消费最多,而且加载数据最多的一种类型,下方是在数据大概2000条左右常规加载和虚拟滚动实现的效果对比:

常规方案在数据量小,dom元素少的情况下,也是非常流畅,但是在数据量达到一定程度,dom元素量过大时,渲染时间就会急剧增多,滚动将变得滞后,灵敏度下降。

四、滚动方式介绍

原理:用固定个数的元素来模拟无线滚动加载,通过位置的布局来达到滚动后的效果

由于无限滚动的列表元素高度是和产品设计的场景有关,有定高的,也有不定高的。

定高:滚动列表中子元素高度相等。

不定高:滚动列表中子元素高度都随机且不相等。

4.1元素定高方案

4.1.1实现原理

如图所示,我们只渲染固定个数的dom元素,一个视口高度是固定的,子元素的高度也是固定的,我们可以推算出一个视口最多可以看到多少个元素

domNum=window.screen.height/itemHeight

在这个基础上上下增加3~5个元素即可,例如一共可以显示4个元素,上下视口溢出各加3,一共需要10个dom元素来实现虚拟滚动。滚动后,每个元素距离父类都会有个偏移高度,默认的高度就是这个元素之上所有的兄弟元素的高度之和,我们这里只采用固定个数的元素,则视口内的元素上面并没有那么多的兄弟,但有需要该元素距离视口有那么多的偏移高度,则通过transform或者top值来把该元素的位置钉住,来模拟滚动后自己需要处在的位置上。

4.1.2滚动效果

4.1.3实现方式

忽略业务相关的任何东西,我们单纯来模拟打造这一套虚拟滚动。我们先通过数组模拟定义2万条数据,并创建一个对象currentArr来表示当前已经获取到的数据。我们模拟一页10条数据,刚进来的时候“后端”放回20条数据,即第一次请求了2页数据(每页10条数据)

...constarr=[];for(leti=0;i<20000;i++){arr.push(i);}constcurrentArr=arr.slice(0,19)//默认取前2页(1页10条数据)...

我们采用3级深度的dom结构

第一层是100vh高度的容器,允许滚动

第二层是所有元素组成高度,可以理解成是一个空有高度的空白元素,这个高度是当前已经获取的所有元素的总高度

第三层是固定元素渲染层

可滚动的元素高度我们需要先撑开。

我们可以动态计算当前元素一共需要多少高度的空间,itemHeight是每一个列表元素的高度,也可以让后端直接返回给我们一共有多少个元素,然后直接全部撑开,那么height则为total*itemHeight

...<divclassName="main"ref={slider}onScroll={throttle(scroll,200)}><divclassName="wrap"style={{height:currentArr.length*itemHeight*2,//这里默认是2倍屏}}>{currentArr.slice(startInex,endIndex).map((item,index)=>{return(<divclassName="item"key={index}style={{position:'absolute',left:0,top:0,transform:`translateY(${(startInex+index)*itemHeight}px)`,}}>{item}</div>);})}</div></div>...

这里需获取要展示数组数据里的起始位置和结束位置

第一个元素位置=滚动距离/列表元素的高度。

最后一个元素位置=第一个元素位置+视口内最多展示元素的个数。

拿到起始位置和结束位置来切割数据数组,每次就取固定个数的元素来进行重绘渲染

constscrollTop=slider.current.scrollTop;//滚动距离letcurrentStartIndex=Math.floor(scrollTop/itemHeight);//起始索引letcurrentEndIndex=currentStartIndex+Math.ceil(screenH/itemHeight);//终点索引

计算元素偏移位置

通过第一个元素的的索引值+当前元素数组索引值可以计算距离父元素顶部的高度,作用在元素的transform属性上,使其定位在固定的高度偏移量上,这样就大功告成了。

<divclassName="item"key={index}style={{position:'absolute',left:0,top:0,transform:`translateY(${(startInex+index)*itemHeight}px)`,}}>

4.1.7整体代码

//index.less.main{width:100vw;jsheight:100vh;overflow:scroll;position:relative;}.item{width:100vw;height:180rpx;border-bottom:2rpxsolidblack;}
//index.jsimport{createElement,useRef,useState}from"rax";import"./index.less";constarr=[];//模拟一共有2万条数据for(leti=0;i<20000;i++){arr.push(i);}//默认第一屏取2页数据constcurrentArr=arr.slice(0,19),screenH=window.screen.height;leti=2,isReqeust=false;functionthrottle(func,delay){vartimer=null;returnfunction(){varcontext=this;varargs=arguments;if(!timer){timer=setTimeout(function(){func.apply(context,args);timer=null;},delay);}}}functionIndex(props){const{itemHeight=90}=props;const[startInex,setStartIndex]=useState(0);const[endIndex,setEndIndex]=useState(9);const[forceUpdate,setForceUpdate]=useState(false);constslider=useRef(null);constscroll=()=>{constscrollTop=slider.current.scrollTop;//滚动距离if(currentArr.length*itemHeight-scrollTop-screenH<400&&!isReqeust){isReqeust=true;//加载下一页数据setTimeout(()=>{currentArr.push(...arr.slice(i*10,i*10+9));i++;scroll();isReqeust=false;setForceUpdate(!forceUpdate)},500)return;}letcurrentStartIndex=Math.floor(scrollTop/itemHeight);//起始索引letcurrentEndIndex=currentStartIndex+Math.ceil(screenH/itemHeight);//终点索引if(currentStartIndex===startInex&&currentEndIndex===endIndex)returnrequestAnimationFrame(()=>{setStartIndex(currentStartIndex)setEndIndex(currentEndIndex)});}return(<divclassName="main"ref={slider}onScroll={throttle(scroll,200)}><divclassName="wrap"style={{height:currentArr.length*itemHeight*2,//这里默认是2倍屏}}>{currentArr.slice(startInex,endIndex).map((item,index)=>{return(<divclassName="item"key={index}style={{position:'absolute',left:0,top:0,transform:`translateY(${(startInex+index)*itemHeight}px)`,}}>{item}</div>);})}</div></div>);}exportdefaultIndex;

五、总结

列表元素等高的方案相对比较容易实现,而且方案很多,有作用在父元素上整体使用transform的,也有作用在每个单一元素上使用transform的,甚至通过top、paddingTop等等各种位置布局属性方案来实现的。

固定元素在渲染上能占到非常大的优势,通过key绑定保障元素性能甚至可以在20个固定元素的场景下渲染做到3ms,但是应用场景往往是多变的,我们的元素等高的场景只是一种,往往还存在非等高的场景,例如微博、Twitter等等,也例如我们上面的对比图,里面的列表元素卡片分非常多种类,这种再操作上就需要换个思路的,不过换汤不换药,对于非等高的列表元素,可以参考虚拟滚动 - 非等高元素无限滚动加载解决方案。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/2582.html