useVirtualList

2020-09-27  本文已影响0人  skoll

虚拟列表的几种实现方式

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>
上一篇 下一篇

猜你喜欢

热点阅读