美文网首页
useVirtualList

useVirtualList

作者: skoll | 来源:发表于2020-09-27 16:29 被阅读0次

    虚拟列表的几种实现方式

    1 .ahooks里面的关键代码

    height:totalHeight-offsetTop,
    marginTop:offsetTop,
    
    1 .只有一层dom结构
    

    2 .一般实现

    1 .一个div来展示总的高度,用来显示滚动条,并接收滚动事件
    2 .下面有一个div,根据滚动条接收到的滚动距离来移动div。这样需要多写一些dom结构
    

    3 .react-virtualized 支持千万级的数据展示
    4 .react-window更快,但是展示的数据没有上面的多

    1 .看css样式变化,好像是分别给每个子div添加绝对定位,通过这个来给元素确定位置,感觉这样计算量有点大。
    2 .https://bvaughn.github.io/react-virtualized/#/components/List
    3 .可以看例子研究下
    4 .react-virtualized 9千万数据的时候就不行了,还是可以滚动,就是会出现延迟显示,偶然的页面卡死
    

    问题,快速拖动滚动条的时候,会出现白屏界面

    1 .

    列表高度不变的代码

    1 .559240,为什么只能显示这么多,后面的就显示不了了。而且页面发现有卡顿。为什么比这个小的数据都是正常显示的
    2 .为什么感觉最大显示数量是和每个item的高度有关的,item越小,显示的越多,做多是160多万条数据
    3 .这个好像真的是这种写法的局限性。100万以上的数据就会显示有问题,总之就是height和margin-top这种显示的问题.ahook本来的组件也是不行,显示不全和卡顿
    4 .最少的显示其实不确定,因为他的正常数据是和每一个子项的高度有关的
    

    性能优化

    1 .超大数组的时候,访问这个数组还是会有性能问题
    2 .百万数据的时候我猜执行性能会在从百万数据截取数据的时候,所以泡个node代码测试下不同数量大小数组使用slice截取数据花费的时间,测试发现和数组大小没关系,花费时间是和截取的多少有明显的关系

    1 .那是什么在影响他的性能呢!
    
    202091918137.png 202091918241.png

    最简单的写法

    import {useEffect,useState,useMemo,useRef,MutableRefObject,useCallback} from 'react'
    import useSize from '../src/useSize'
    
    export interface OptionType {
        itemHeight:number
        overscan?:number,
    }
    
    export default function useVirtuallList<T = any>(list:T[],options:OptionType){
        const containerRef = useRef<HTMLElement | null>()
        const size=useSize(containerRef as MutableRefObject<HTMLElement>)
        const [state,setState]=useState({start:0,end:10})
        const {itemHeight,overscan=0}=options
        let len=list.length
        
        const getViewCapacity=useMemo(()=>{
            if(containerRef.current){
                return Math.ceil(containerRef.current.clientHeight/itemHeight)
            }return 10        
        },[itemHeight,containerRef.current])
         
        const getOffset=(scrollTop:number)=>{
            return Math.floor(scrollTop/itemHeight)+1
        }
    
        const calculateRange=()=>{
            const el=containerRef.current
            if(el){
                const offset=getOffset(el.scrollTop)            
                const from=offset-overscan
                const to=offset+getViewCapacity+overscan
    
                setState({
                    start:from<0?0:from,
                    end:to>len?len:to,
                })
            }
        }
    
        const totalHeight=useMemo(()=>{
            return len*itemHeight
        },[len,itemHeight])
        // 获取全部元素的高度
    
        const getDistanceTop=useCallback((index:number)=>{
                return index*itemHeight
        },[itemHeight])
        // 根据当前的index元素,算出应该滚动的高度
    
        // const scrollTo=(index:number)=>{
        //     if(index===0)return
        //     if(containerRef.current){
        //         containerRef.current.scrollTop=getDistanceTop(index)
        //         calculateRange()
        //     }
        // }
    
        // 滚动到index位置
        // const scrollBy=(distance:number)=>{
        //     if(containerRef.current){
        //         containerRef.current.scrollBy(0,distance)
        //         calculateRange()
        //         // 这里有异步操作,会导致滑动的时候没有数据操作,当不在最下面的时候,是正常移动的,但是每当有新增数据的时候,往往慢半拍
        //         // 所以还是加一个参数来实现是否每次新增数据都移动的操作吧
        //     }
        // }
        //以一定距离进行滚动
    
        const offsetTop=useMemo(()=>{
            return state.start*itemHeight
        },[itemHeight, state.start])
    
        const showList=useMemo(()=>{
            return list.slice(state.start,state.end)
        },[state.start,state.end])
    
        useEffect(()=>{
            calculateRange()
        },[size.width,size.height])
    
        return {
            list: showList,
            containerProps: {
                ref: function(ele: any){
                  containerRef.current = ele;
                },
                style: { overflowY: 'auto' as const },
                onScroll:(e:any)=>{
                    e.preventDefault()                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
                    calculateRange()
                }
            },
            wrapperProps:{
                style:{
                    width:'100%',
                    height:totalHeight-offsetTop,
                    marginTop:offsetTop,
                }
            },
    
        }
    }
    

    另一种写法

    1 .这种最大也是80多万。到底是那里的极限被碰到了
    2 .都是下面还有元素,但是父组件的滚动条已经到底了,不能继续向下滚动了
    3 .33554400 渲染一个div最大的值就是这么大,height最大值 16777100他们最大的高度是这么大,我就知道不能一直大的
    4 .https://stackoverflow.com/questions/16637530/whats-the-maximum-pixel-value-of-css-width-and-height-properties

    问题

    1 .transform3d滑动的时候都会出现抖动,想要react-spring来包装一下滑动,感觉需要加一个动画函数.但是用不出来...
    2 .scroll-behavior: smooth .最后加个这个属性先勉强一下吧
    3 .限制频率:最小一次滚动鼠标,会移动100px的距离,但是看日志确实触发了很多次scroll函数,其实中间的根本不必要触发,因为这里的计算结果我们根本不需要,所以这里可以做下频率限制.还有一个问题就是,既然我们是以100px的最小距离往下滑动,在列表里面,如果列表的高度是小于100px的,那么可能会在滑动的时候直接跳过这一项不显示。加了限制频率的函数发现滚动又不流畅了。。
    4 .所以还是滚动的那里不加,计算现实的那个加吧。但是这里的函数是需要传值的,限流函数好像都一直不能传参数的28
    5 .// willChange:'transform':加了这个属性,滚动的时候会出现不流畅的情况。就是惯性停留在原地。感觉这个加错地方了,应该加载移动的子元素上面,而不是加载滚动条div上willChange:'scroll-position',
    6 .https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change

    1 .不能将will-change应用在太多的元素上,过度使用会导致页面响应速度缓慢或者消耗非常多的资源
    2 .当元素变化前和之后通过脚本来切换是否应用这个属性,不要让他一直挂在元素上面
    3 .不要太早的使用这个属性,这个是最后的保障,初期为了一点速度就使用这个是不合适的,应该先去尝试解决真正的性能问题
    4 .
    

    最终版本,使用百分比决定相对显示的数据

    import {useEffect,useState,useMemo,useRef,MutableRefObject,useCallback} from 'react'
    import useSize from '../src/useSize'
    import {useThrottleFn} from 'ahooks'
    
    export interface OptionType {
        itemHeight:number
        overscan?:number,
    }
    
    let maxHeight=16777100
    // 别的库最大高度就是这个.
    
    export default function useVirtuallList<T = any>(list:T[],options:OptionType){
        const containerRef = useRef<HTMLElement | null>()
        const size=useSize(containerRef as MutableRefObject<HTMLElement>)
        const [state,setState]=useState({start:0,end:10})
        const [scrollTop,setScrollTop]=useState(0)
        
        // 每个补偿的高度,默认是0
    
        const {itemHeight,overscan=0}=options
        let len=list.length
    
        const getLen=useMemo(()=>{
            return len.toString().length
        },[len])
    
        const getNum=useMemo(()=>{
            let num=1
            for(let i=0;i<getLen;i++){
                num*=10
            }
            return num
        },[getLen])
        
        const getViewCapacity=useMemo(()=>{
            if(containerRef.current){
                return Math.ceil(containerRef.current.clientHeight/itemHeight)
            }return 10 
        },[itemHeight,containerRef.current])
         
        const {run}=useThrottleFn(()=>{            
                let from=(Number((scrollTop/totalHeight).toFixed(getLen))*getNum)-overscan
                // 还需要判断下是不是整数,看log发现还是有异常情况7.000000000000001.100以内的情况
                if(!Number.isInteger(from)){
                    from=Math.floor(from)
                }
                let to=from+getViewCapacity+overscan
                setState({
                    start:from<0?0:from,
                    end:to>len?len:to,
                })
    
        },{
            wait:100
            // 感觉这么大还是跟手。一次鼠标滑动,这里触发3次
        })
    
        const calculateRange=()=>{
            const el=containerRef.current
            if(el){
                if(el.scrollTop>totalHeight){
                    console.log('你不对劲')
                    return
                }else{
                    setScrollTop(el.scrollTop)        
                    run()
                }
                
            }
        }
        const totalHeight=useMemo(()=>{
            let totalHeight=len*itemHeight
            if(totalHeight>maxHeight){
                return maxHeight
            }return len*itemHeight
        },[len,itemHeight])
    
        const showList=useMemo(()=>{
            return list.slice(state.start,state.end)
        },[state.start,state.end,list])
    
        
        useEffect(()=>{
            calculateRange()
        },[size.width,size.height])
    
        return {
            list: showList,
            containerProps: {
                ref: function(ele: any){
                  containerRef.current = ele;
                },
                style: { 
                        overflowY: 'auto' as const,
                        position:'relative'
                    },
                onScroll:(e:any)=>{
                    e.preventDefault()                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
                    calculateRange()
                }
            },
            wrapperProps:{
                style:{
                    width:'100%',
                    position:'absolute' as 'absolute',
                    top:0,
                    transform:`translate3d(0,${scrollTop}px,0)`,
                    willChange:'transform',
                }
            },
           scrollBarProps:{
               style:{
                   height:totalHeight,
                   willChange:'scroll-position',
               }
           }
        }
    }
    
    function VirtualListItem(props:any){
      const itemRef=useRef<HTMLDivElement|null>(null)
      useEffect(()=>{
        // if(!props.arrHeight[props.index]){
        //   console.log('添加了')
        //   // 这里其实还需要加上margin的高度,但是算的话有点没必要,反正这个值也是固定还
        //   if(itemRef.current){
        //     props.updateAt(props.index,itemRef.current.clientHeight+10)
        //     // 这里只有一个margin-bottom:所以只加10,要是别的就在这里另外算
        //     // 每次都要特殊设置
        //   }
        // }
        // 这里不用做判断了,就让他每次都更新吧。
    
        // if(itemRef.current){
        //   props.updateAt(props.index,itemRef.current.clientHeight+10)
        //   // 这里只有一个margin-bottom:所以只加10,要是别的就在这里另外算
        //   // 每次都要特殊设置
        // }
    
        // console.log('首次创建',console.log(itemRef.current?.clientHeight))
      },[])
      return (
        <div  ref={itemRef} 
              style={{
                        height: 20,
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                        // marginBottom: 8,
                        // marginTop:8,
                        wordBreak: 'break-word',
                      }}
              >
          {props.children}
        </div>
      )
    }
    
    
      const {list,containerProps,wrapperProps,scrollBarProps}=useList(arrList,{
        overscan:1,
        itemHeight:40,
      })
    <div 
              {...containerProps}
              style={{
                         height:'200px',
                         width:'500px',
                        //  willChange:'scroll-position',
                         overflow:"auto",
                         position:'relative',
                         scrollBehavior: "smooth",
                         overflowAnchor:"none",
                         }}>
              <div {...scrollBarProps}></div>
              <div {...wrapperProps}>
                {list.map((el)=>(
                  <div  key={el}
                        style={{
                          height:40,
                          display: 'flex',
                          justifyContent: 'center',
                          alignItems: 'center',
                        }}
                  >
                      {el}
                  </div>
                ))}
              </div>
            </div>
    

    相关文章

      网友评论

          本文标题:useVirtualList

          本文链接:https://www.haomeiwen.com/subject/pyepektx.html