【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
  • 代碼裏紅色那個滾動條沒去,可以把樣式給去了。

拓展

  • 這個組件是固定寬高的虛擬化組件,如果組件未固定寬高,那就耗費更多資源計算。如果有時間下次寫着玩。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章