美文网首页
useVirtualList

useVirtualList

作者: skoll | 来源:发表于2022-03-09 10:20 被阅读0次

简介

1 .虚拟化列表能力的hook,用于展示海量数据渲染时首屏渲染缓慢和卡顿的问题

扩展

1 .和无限列表相结合,每次滚动到最下方,就拉取一些新的元素

import React,{useEffect,useState,useRef,useMomo} from 'react'
import {useVirtualList,useInViewport} from 'ahooks'

export default function Drag(props){
    const containerRef=useRef()
    const wrapperRef=useRef()
    const footerRef=useRef()

    const [origin,setOrigin]=useState([1,2,3,4,5,6,7,8,9,10])
    const [inViewport]=useInViewport(footerRef)

    const [list]=useVirtualList(origin,{
        containerTarget:containerRef,
        wrapperTarget:wrapperRef,
        itemHeight:60,
        overscan:1,
    })
    
    function handleAdd(){
        let data=[]
        for(let i=origin.length+1;i<origin.length+5;i++){
            data.push(i)
        }

        // 假设在请求资源
        setTimeout(()=>{
            setOrigin([...origin,...data])
            console.log(origin,'当前')
        },2000)
        
    }

    useEffect(()=>{
        console.log(inViewport,'outer加一组数据')
        if(inViewport){
            console.log(inViewport,'inner加一组数据')
            handleAdd()
        }
    },[inViewport])
    return (
        <div>
            <div ref={containerRef} style={{height:'200px',overflow:'auto',border:'1px solid #eee'}}>
                <div ref={wrapperRef}>
                    {list.map((el)=>{
                        if(el.index===origin.length-1){
                            return <div  ref={footerRef} style={{color: inViewport ? '#87d068' : '#f50' ,
                                height: 52,
                                display: 'flex',
                                justifyContent: 'center',
                                alignItems: 'center',
                                border: '1px solid #e8e8e8',
                                marginTop: 8,
                            }}
                            onClick={handleAdd}
                            // 万一没有触发,就加一个手动的点击
                            >
                            {inViewport?"正在加载":"更多"}
                        </div>
                        }else{
                            return <div
                            key={el.index}
                            style={{
                                height: 52,
                                display: 'flex',
                                justifyContent: 'center',
                                alignItems: 'center',
                                border: '1px solid #e8e8e8',
                                marginTop: 8,
                            }}
                            >{JSON.stringify(el)}</div>
                        }
                    })}
                    
                </div>
                
            </div>
            <button onClick={handleAdd}>{inViewport?'visible':'hidden'}</button>

        </div>
    )
}


2 .问题1,const [inViewport]=useInViewport(footerRef) 发现元素出现的时候会弹好几次参数


image.png
1 .margin的时候就算出现了,不精确

3 .问题2,const [inViewport]=useInViewport(footerRef)首次没渲染的时候,有这个标记,就会里面拉取一下.导致一开始就有问题

这里这个可以当作loading界面,初次加载的时候这里数据默认为空,一到这个页面,发现在里面就去加载数据吧,加载了新数据把他顶到下面,看不见
1 .首次加载的时候他的值变化是undefind,true,false

3 .问题2 ,之前最麻烦的时候东西就是动态高度.这就要求.请求到数据,算出来他的高度,然后把高度加到原来的里面,这时候算完,再把他当成一个知道高度的放到原来的里面.看别的库的api或者思路来说,是会有一个固定高度的,先用这个,然后大小发生改变的时候,算出改变之后的高度,数据也是动态增加的,不是一直就有的

MutationObserver API
import React,{useEffect,useState,useRef} from 'react'
import {useVirtualList,useInViewport} from 'ahooks'

export default function Drag(props){
    const [obj,setObj]=useState({})
    const id='1'
    useEffect(()=>{
        setTimeout(()=>{
            setObj(Object.assign({},{name:'刘华强',age:20}))
        },1000)


        let observer=new MutationObserver(()=>{
            const {height} =document.getElementById(id).getBoundingClientRect()
            console.log('发生了变化',height)
            // 然后把这个值给广播出去
        })

        observer.observe(document.getElementById(id),{
            childList:false,
            // 子节点的新增,删除,更改能否被检测
            attributes:false,
            //属性的变动能否被检测
            characterData:true,
            // 节点内容或节点文本的边动能否被检测:我们这里是只会有文字新增,所以这里选这个,别的检测都关掉
            subtree:true,
            // 是否应用于该节点的所有后代节点:为啥必须是这个
        })

        return ()=>{
            observer.disconnect()
            // 组件消除的时候取消观察
        }
    },[])

    return (
        <div>
            <div id={id} style={{lineHeight:20}}>
                {JSON.stringify(obj)}
            </div>
        </div>
    )
}


//方法2:在数据变化的时候,求大小,但是这不是加在回调里面,感觉可能不会很准,虽然现在能看到值,不是很保险,但是简单
 const [obj,setObj]=useState({})
    const id='1'
    useEffect(()=>{
        setTimeout(()=>{
            setObj(Object.assign({},{name:'刘华强',age:20}))
        },1000)
        const node=document.getElementById(id)
        const height=node.getBoundingClientRect().height
        console.log(height)
    },[])
    return (
        <div>
            <div id={id} style={{lineHeight:20}}>
                {JSON.stringify(obj)}
            </div>
        </div>
    )

问题3 .添加数据的时候,进度条高度会有闪动,不是很稳定,用react-window来实现一下这个吧

import React,{useEffect,useState,useRef} from 'react'
import { VariableSizeList as List } from 'react-window'

export default function Drag(props){
    // 高度不确定
    // 动态增加数据
    const data=[]
    //全部的数据,会动态增加

    const rowSize=[]
    // 全部item的高度

    function getItemSize(index){
        console.log('idx',index,rowSize[index-1])
        if(rowSize[index]){
            return rowSize
        }else{
            return 21.97
        }
    }

    function Row({index,style}){
        const rowRef=useRef()
        useEffect(()=>{
            let observer=new MutationObserver(()=>{
                const {height} =rowRef.current.getBoundingClientRect()
                rowSize[index]=height
                console.log('触发了变化',rowSize)
            })
            
            rowSize[index]=rowRef.current.getBoundingClientRect().height
            console.log(rowRef.current,'初始的高度',rowSize)
            observer.observe(rowRef.current,{
                childList:false,
                // 子节点的新增,删除,更改能否被检测
                attributes:false,
                //属性的变动能否被检测
                characterData:true,
                // 节点内容或节点文本的边动能否被检测:我们这里是只会有文字新增,所以这里选这个,别的检测都关掉
                subtree:true,
                // 是否应用于该节点的所有后代节点:为啥必须是这个
            })

            return ()=>{
                observer.disconnect()
                // 组件消除的时候取消观察
            }
        },[])

        // setTimeout(()=>{
        //     setValue(index+Math.random()*1000)
        // },1000) 

        return (
            <div style={style} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} ref={rowRef}>
                {/* 子项里面的style,必须要绑定,可以看成是需要从父元素继承他需要算的一些东西 */}
                Row {index}<br/>
                {index%2?<span>加一行</span>:""}
            </div>
        )
    }

    const Example=()=>(
        <List
            height={200}
            itemCount={20}
            width={500}
            itemSize={getItemSize}
        >
            {Row}
        </List>
    )
    return (
        <div>
            <Example />
        </div>
    )
}


问题4 .useRef无效的问题:结果是绑定错了

import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const observed = useRef(null);

  useEffect(() => {
    console.log(observed.current);
  }, [observed]);

  return (
    <div ref={observed} className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

react-window不支持动态高度的

const rowHeights = new Array(1000)
  .fill(true)
  .map(() => 25 + Math.round(Math.random() * 50));
 
const getItemSize = index => rowHeights[index];

1 .最关键的是这种每一个元素的动态高度是拿不到的.如何提前知道呢,难道是没显示的时候就先渲染出来么..感觉也是不可能的
2 .还有就是渲染出来告诉父组件这个现在的高度,然后重新刷新.但是问题来了

<div style={style} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} ref={rowRef}>
                {/* 子项里面的style,必须要绑定,可以看成是需要从父元素继承他需要算的一些东西 */}
                Row {index}<br/>
                {index%2?<span>加一行</span>:""}
            </div>
//子组件需要接收style,这个style就是父组件传下来的每一个子元素的css样式,已经定死了height,搞得你没法算最新的高度,而且就算算出来也不一定能更新出来

useVirtualList实现动态高度

import React, { useMemo, useRef,useEffect,useCallback,useState} from 'react';
import { useVirtualList } from 'ahooks';

export default function Drag (){
  const containerRef = useRef();
  const wrapperRef = useRef();
  const originalList = useMemo(() => Array.from(Array(20).keys()), []);
  
  const itemHeightArray=useRef([])
  //存储了所有的高度  
  const lastComputed=useRef(0)


  const [list,] = useVirtualList(originalList, {
    containerTarget: containerRef,
    wrapperTarget: wrapperRef,
    itemHeight: (i) =>{
        // if(itemHeightArray.current[i]){
        //     return itemHeightArray.current[i]
        // }else{
        //     return 21.99
        // }
        return 42
    },
    overscan: 10,
  });

  function Item(props){
        const itemRef=useRef()
        useEffect(()=>{
            if(props.ele.index<lastComputed.curent){
              return ()=>{
                console.log('已经做过计算了')
              }
            }else if(props.index>lastComputed.current){
              lastComputed.current=props.ele.index
            }


            let observer=new MutationObserver(()=>{
                console.log('发生了变化')
            })

            // 初次加载的时候更新一波高度
            itemHeightArray.current[props.ele.index]=itemRef.current.getBoundingClientRect().height

            observer.observe(itemRef.current,{
                childList:false,            
                attributes:false,             
                characterData:true,
                subtree:true,              
            })

            return ()=>{
                observer.disconnect()
            }
        },[])

      return (
        <div    
              ref={itemRef}
              style={{
                height: props.ele.index % 2 === 0 ? 42 :42,
                // 高度做成自定义的
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
                border: '1px solid #e8e8e8',
                marginBottom: 8,
                lineHeight:1.5,
              }}
              key={props.ele.index}
            >
              Row: {props.ele.data}<br/>
              {props.ele.index % 2 === 0 ? 'small' : ''}
            </div>
      )
  }
  return (
    <div>
      <div ref={containerRef} style={{ height: '300px',
       overflow: 'auto',
       width:400 }}>
        <div ref={wrapperRef}>
          {list.map((ele) => (
              <Item ele={ele}/>
          ))}
        </div>
      </div>
    </div>
  );
};

性能分析

image.png

1 .脚本执行花费了那么多时间吗?去掉计算动态高度的代码,也是这么多脚本执行时间,问题出在哪里呢


image.png

2 .不新开函数立马就下来了,难道还和这个有关系?离谱啊

 function It(){
    return (
      <div>
        测试1
      </div>
    )
  }
//竟然真的是这样,这么简单的一个组件,里面没有任何逻辑,按照理解应该是和上面的一样啊,竟然脚本计算时间瞬间就上去了.但是在官方的demo里面这样改就没问题,脚本计算没这个爆炸.为啥呢

3 .用新函数开一个item性能这么差么
4 .普通渲染的差距看下,在普通的列表里面,是没关系的.那就是这个组件里面的差距,这感觉要翻下源码.
5 .组件的时候scroll事件占了大部分的时间
6 .不用组件的时候,出现的事件确是whell事件.还有指针事件,算了,看下他的源码吧,为什么框架里面只绑定了scroll事件,但是会触发各种各样的事件呢.pointerout,mouseout,scroll

最后

1 .目前是想要的效果实现了,帧数有时候会跑不满60,需要优化
2 .还有就是会出现布局抖动的问题,这个是看浏览器出现的,后续也要研究下

相关文章

  • useVirtualList

    虚拟列表的几种实现方式 1 .ahooks里面的关键代码 2 .一般实现 3 .react-virtualized...

  • useVirtualList

    简介 1 .虚拟化列表能力的hook,用于展示海量数据渲染时首屏渲染缓慢和卡顿的问题 扩展 1 .和无限列表相结合...

网友评论

      本文标题:useVirtualList

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