【React】手写虚拟化无限滚动组件

前言

  • react的虚拟化无限滚动有别人写好的,antd的list组件里也有这种虚拟化组件,antd虚拟化列表地址
  • 由于上次写了一篇浅显的滚动性能优化后发现这种技术可以更大的提升效率,并且上次那篇的蛮力实现的思维走入了死胡同,而这种方案让我立马来了兴趣,想要实现一番。

原理

  • 原理就和我在那篇评论里留言的差不多,就是每次只显示固定的几个div,滚动时把div给换掉。

  • 这东西实现起来难点有下面几个:
    一、滚动条的出现
    二、可视区域流畅移动
    三、闪屏优化

  • 这玩意就不按步骤写了,主要说一下这几个难点是怎么解决的:

滚动条出现

  • 这个难点最简单,但是思路没打开就容易想不到这种虚拟化实现方法。靠空白的div做的滚动条。
  • 这主要还是和布局有关,目前有几种布局可以参考:
  • 第一种就是滚动条和列表并排,这样完全就可以撑起来了。
  • 第二种是滚动条打底,列表项绝对定位,因为本来列表项就需要进行移动来配合滚动条,所以列表项绝对定位也是可以的。
  • 我组件里采用第一种方式。

可视区域流畅移动

  • 第二个难点在可视区域的移动上,因为按虚拟化的方案,可视区域是和滚动条一起走的,但是还要兼顾起始项高度与每一项的高度,不然就变成可视区域fixed在屏幕上的奇怪现象。

  • 为了滚动更加流畅,还需要估算一个滚动到每个元素的百分比,不然会出现可视区域瞬间所有项目改变的奇怪现象。百分比使用余数进行计算,可以产生滚动条滚在当前元素的百分之几,然后这个百分比乘每行高度得到需要减去的距离。

闪屏优化

  • 这个是最难的,耗费我好几个小时,还走了很多误区。
  • 解决了前面2个难点,基本上已经可以工作了,但是会出现闪屏。
  • 根据我打断点发现,闪屏原因是监听scroll的函数里设置索引,然后刷新页面,索引改动后,useEffect触发执行,将该渲染的元素渲染页面上。这样造成2次渲染,但是,监听scroll函数里拿不到props.children,因为这个children传过来的值并不是初始就可以取到。而children改变后,scroll的props.children还是最早的那个值。
  • 我试了useState把值存进去,scroll去useState里拿props.children,结果无效。
  • 试了useState存函数,结果会造成无限递归。。
  • 试了改函数作用域,用bind取this,搞了半天都不行。
  • 最后突然想到,干嘛非要执着在scroll里拿children,我让它延迟渲染不就行了,结果加了setTimeout完美解决问题。

效果

  • 我把占滚动条的div做成红色,为了方便观看,到时候把样式去了就行。可以看见,完美配合分段加载以及IntersectionObserver,这几个一起用完全不会冲突。这个案例同时用了这3个技术。
    在这里插入图片描述

代码

  • 使用的话就传几个值就可以了,中间该怎么循环写列表就怎么写。
            <Virtualize itemHeight={rootSize*(4.5596)} columnNumber={2}
                insightNumber={6} startHeight={rootSize*6}
                scrollDom={document.querySelector('.home-main-container')}
            >
            {
                props.renderProduct.productList.map((item:Product,index:number)=>{
                  return (                     
                        <Link key={item.id} to={{pathname:`/productdetail/${item.id}`,state:item}}
                        >
                        <Card
                            hoverable
                            key={item.id}
                            cover={
                             <div  data-src={item.poster}
                             className='list-item'
                             ref={(ref)=>{
                                if(ref){
                                    io!.observe(ref)
                                }}}                              
                             >
                                <Icon type="picture"></Icon>
                             </div>                                                        
                            }
                        ><Card.Meta title={item.title} description={`价格:${item.price}元`} />
                        </Card>
                        </Link>
                    )                 
                  })
            }
           </Virtualize>
  • 传入参数都有注释
type Props = PropsWithChildren<{
    itemHeight:number//每个元素高
    columnNumber:number//一行几个元素
    insightNumber:number//可视范围里几个元素
    startHeight:number//滚动到第一个元素的高度
    scrollDom:HTMLDivElement|null //有滚动条的dom
    scaleRow?:number//扩展行数
}>

function  Virtualize(props:Props){
    const [costomHeight,setCostomHeight]=useState()
    const [visbleHeight,setVisibleHeight]=useState()
    const [renderChildren,setRenderChildren]=useState()
    const [indexNumber,setIndexNumber]=useState({
        startIndex:0,
        endIndex:props.insightNumber,
        overScroll:0
    })
    const [scaleRow,setScaleRow]=useState(2)
    useEffect(()=>{
        if(props.children instanceof Array){
            let childrenLen = props.children.length
            if(childrenLen%props.columnNumber!=0){//说明最后一行没满
                let remain = childrenLen%props.columnNumber
                childrenLen=childrenLen+remain
            }
            let fullheight = childrenLen/props.columnNumber*props.itemHeight
            setCostomHeight(fullheight)
            let insightHeight
            if(childrenLen<props.insightNumber){
                insightHeight = fullheight
            }else{
                insightHeight = props.insightNumber/props.columnNumber*props.itemHeight
            }
            setVisibleHeight(insightHeight)
            setRenderChildren(props.children.slice(indexNumber.startIndex,indexNumber.endIndex))
        }
    },[props.children,indexNumber])
    const scrollFunc=(e:Event)=>{
        let target= e.target as HTMLDivElement
        let overScroll = target.scrollTop-props.startHeight//卷曲高度
        let timer = overScroll/props.itemHeight*props.columnNumber
        let startIndex =Math.floor(timer)//起始索引 从0开始
        startIndex = startIndex<0?0:startIndex;
        timer = timer%props.columnNumber/props.columnNumber//滚的每行百分比
        if(timer<0)timer=0;
        if(overScroll<0)overScroll=0
        if(startIndex%props.columnNumber!=0){//每行没补满
            startIndex=startIndex-startIndex%props.columnNumber
        }
        let endIndex = startIndex+props.insightNumber+scaleRow
        overScroll=overScroll-timer*props.itemHeight
        setTimeout(() => {
            setIndexNumber({
                startIndex,
                endIndex,
                overScroll
            })
        });
      
    }
    useEffect(()=>{
        props.scaleRow?setScaleRow(props.scaleRow):null;
        if(props.scrollDom)
        props.scrollDom.addEventListener('scroll',throttle(scrollFunc,50))
        return ()=>{
            if(props.scrollDom)
            props.scrollDom.removeEventListener('scroll',throttle(scrollFunc,50))
        }
    },[])
    return (
       <>
       <div style={{display:'flex'}}>
       <div style={{height:costomHeight?costomHeight:0,backgroundColor:'red',width:'20px'}} ></div>
       <div className='virtual-custom-item' 
       style={{
           height:visbleHeight?visbleHeight:0,
           position:"relative",
           transform:`translate3d(0px, ${indexNumber.overScroll}px, 0px)`
        }}>
       {renderChildren}
       </div>
       </div>
     
       </>
    )
}
export default Virtualize
  • 代码里红色那个滚动条没去,可以把样式给去了。

拓展

  • 这个组件是固定宽高的虚拟化组件,如果组件未固定宽高,那就耗费更多资源计算。如果有时间下次写着玩。
发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/104809316