当前位置: 首页 > Web前端 > JavaScript

大家看得懂的源码ahooks使用VirtualList封装了一个虚拟的滚动列表

时间:2023-03-27 10:39:07 JavaScript

本文是深入浅出的ahooks源码系列文章的第十八篇。本系列已整理成文档地址。觉得还不错,给个star支持一下,谢谢。Hook提供虚拟化列表能力,用于解决海量数据渲染时首屏渲染慢、滚动卡顿的问题。详情可查看官网,文章源码可点击此处。实现原理实现原理是监听外部容器的scroll事件,当其大小发生变化时,触发计算逻辑计算内部容器的height和marginTop值。其监听和滚动逻辑的具体实现如下://当外部容器大小发生变化时,触发计算逻辑useEffect(()=>{if(!size?.width||!size?.height){return;}//重新计算逻辑calculateRange();},[size?.width,size?.height,list]);//监听外部容器的滚动事件useEventListener('scroll',e=>{//如果是直接跳转,不需要重新计算/外部容器目标:containerTarget,},);其中,calculateRange非常重要。基本实现了虚拟滚动的主要流程逻辑。它主要做了以下几件事:获取整个内部容器的totalHeight。根据外层容器的scrollTop计算已经“滚动”了多少item,值为offset。根据外部容器的高度和当前起始索引,得到外部容器可承载个数的visibleCount。并根据过扫描(视口上方和下方显示的额外DOM节点数)计算起始索引(start)和(end)。根据起始索引,得到距起始点的距离(offsetTop)。最后根据offsetTop和totalHeight设置内层容器的height和marginTop值。变量有很多,结合下图可以清楚的理解:代码如下://计算范围,从哪开始,从哪结束constcalculateRange=()=>{//获取外容器和内容器//外层容器constcontainer=getTargetElement(containerTarget);//内部容器constwrapper=getTargetElement(wrapperTarget);if(container&&wrapper){const{//从顶部滚动的距离。设置或获取对象顶部与窗口中可见内容顶部的距离scrollTop,//内容可见区域的高度clientHeight,}=container;//根据外部容器的scrollTop计算已经“滚动”了多少itemconstoffset=getOffset(scrollTop);//DOM可见区域个数constvisibleCount=getVisibleCount(clientHeight,offset);//开始下标conststart=Math.max(0,offset-overscan);//结束下标constend=Math.min(list.length,offset+visibleCount+overscan);//获取顶部高度constoffsetTop=getDistanceTop(start);//设置内层容器的高度,总高度-顶部高度//@ts-ignorewrapper.style.height=totalHeight-offsetTop+'px';//margintop是顶部高度//@ts-ignorewrapper.style.marginTop=offsetTop+'px';//设置最后显示的ListsetTargetList(list.slice(start,end).map((ele,index)=>({data:ele,index:index+start,})),);}};其他都是该函数的辅助函数,包括:根据外容器的高度和内部的每一项计算可见区域的个数://根据外容器的高度和内部的每一项计算可见区域的个数内部的每个项目constgetVisibleCount=(containerHeight:number,fromIndex:number)=>;{//知道每一行的高度-number类型,然后根据容器计算if(isNumber(itemHeightRef.current)){returnMath.ceil(containerHeight/itemHeightRef.current);}//动态指定每个元素的高度letsum=0;让endIndex=0;for(leti=fromIndex;i=containerHeight){break;}}//最后一个下标减去第一个下标returnendIndex-fromIndex;};根据scrollTop计算其上有多少个DOM节点//根据scrollTop计算其上有多少个DOM节点constgetOffset=(scrollTop:number)=>{//每个item固定高度if(isNumber(itemHeightRef.current)){返回Math.floor(scrollTop/itemHeightRef.current)+1;}//动态指定每个元素的高度letsum=0;让偏移量=0;//从0开始for(leti=0;i=scrollTop){offset=i;休息;}}//最后一个满足要求的+1返回偏移量+1;};获取顶部高度//获取顶部高度constgetDistanceTop=(index:number)=>{//每个项目具有相同的高度if(isNumber(itemHeightRef.current)){constheight=index*itemHeightRef.current;返回高度;}//动态指定每个元素的高度,那么itemHeightRef.current是一个函数constheight=list.slice(0,index)//reduce计算总和//@ts-ignore.reduce((sum,_,i)=>sum+itemHeightRef.current(i,list[index]),0);返回高度;};计算总高度//计算总高度consttotalHeight=useMemo(()=>{//每个item高度相同if(isNumber(itemHeightRef.current)){returnlist.length*itemHeightRef.current;}//动态指定每个元素的高度//@ts-ignorereturnlist.reduce((sum,_,index)=>sum+itemHeightRef.current(index,list[index]),0,);},[list]);最后暴露一个滚动到指定索引的函数,主要是计算索引距离顶部scrollTop的高度设置到外部容器,并触发calculateRange函数。//滚动到指定的索引constscrollTo=(index:number)=>{constcontainer=getTargetElement(containerTarget);如果(容器){scrollTriggerByScrollToFunc.current=true;//滚动container.scrollTop=getDistanceTop(index);计算范围();}};思考与总结对于高度比较确定的情况,我们做虚拟滚动比较简单,但是如果高度不确定呢?或者换个角度,当我们的滚动不是垂直的,而是水平的,我们应该怎么处理呢?本文已收录在我的个人博客中,欢迎关注~