React实现虚拟滚动列表

一、实现原理

1、原理很简单,核心原理就是显示一小部分数据,比如原数据有10000条,滚动的过程中只截取其中10条进行展示。
2、再细一点说,就是获取到滚动条的滚动距离和行高,滚动距离 / 行高 = 滚动行数,比如滚动行数 = 10,当前展示的就是 list.slice(10, 20)的数据。
3、模拟滚动:html结构如下,在首次渲染时计算出 li 的平均高度rowHeight,滚动元素 ul 的高度 = list.length * rowHeight;这样设置了高度,滚动条的位置就是真实的,在滚动监听onScroll中设置元素ul的paddingTop = div 的 scrollTop,就模拟了滚动。

  • {item}
style: .virtualized { position: relative; overflow-y: auto; border: 1px solid #ddd; height: 200px; }

二、代码

index.jsx

/**
 * VirtualList
 * 虚拟滚动列表--lvxh
 * 
 * list: 需要展示的数据
 * style: 最外层样式
 * rowHeight:初始的行高,默认值50
 * hasMoreCol:一行是否有多列
 * colWidth:初始列宽,默认值100
 * ulRender:自定义ul列表,函数类型 (showList, ulStyleStr) => volid,其中 showList 是最终展示的部分数据,ulStyleStr 是ul的style
 * liRender:自定义li,函数类型 (item) => volid,item是展示的数据项
 * 
 * 一般而言建议自定义li,
 * 如果没有自定义ul和li,则默认展示
  • {item}
  • ,所以item需要是能正确展示的数据类型 */ import { useRef, useEffect, useState, useCallback, useMemo } from 'react' import styles from './index.less' const LIST = new Array(100000).fill().map((item, i) => (`test-data-${i + 1}`)) const getPropertyValue = (node, styleName) => { return window.getComputedStyle(node)?.getPropertyValue(styleName) } export default ({ list = LIST, style, rowHeight = 50, colWidth = 100, hasMoreCol, ulRender, liRender }) => { const boxRef = useRef(null) const [num, setNum] = useState(10) const [colNum, setColNum] = useState(1) const [showList, setShowList] = useState([]) const [paddingTop, setPaddingTop] = useState(0) const [_rowHeight, setRowHeight] = useState(rowHeight) const [_colWidth, setColWidth] = useState(colWidth) const [beyondDistance, setBeyondDistance] = useState(0) // 滚动到底部时,最后一个元素超出可视区域的距离 // 设置可视区域数据 const setData = useCallback((startIndex, endIndex) => { setShowList(list.slice(startIndex, endIndex)) // 取出要渲染的数据 }, [list]) // 计算num、colNum const calculateNum = useCallback((colW, rowH, clientHeight, clientWidth) => { const _rowNum = Math.floor((clientHeight / rowH) + 2) let _num = _rowNum if (hasMoreCol) { const _colNum = Math.floor(clientWidth / colW) setColNum(_colNum) _num = _colNum * _rowNum } setData(0, _num) setNum(_num) }, [hasMoreCol, setData]) // 初始化渲染完成后立马计算出真实的行高和列宽,取初始数据的中位数,列宽取最大值 const calculateSize = useCallback((children, ...ret) => { const len = children.length if (len > 0) { const widthArr = [] const heightArr = [] children.forEach((child) => { const { width, height } = child.getBoundingClientRect() const marginTop = getPropertyValue(child, 'margin-top').slice(0, -2) const marginLeft = getPropertyValue(child, 'margin-left').slice(0, -2) heightArr.push(Math.floor(height + marginTop * 2)) widthArr.push(Math.floor(width + marginLeft * 2)) }) const h = heightArr.reduce((total, cur) => total + cur) / len const w = widthArr.reduce((total, cur) => total + cur) / len setRowHeight(h) setColWidth(w) calculateNum(w, h, ...ret) } }, [calculateNum]) // 初始化,根据初始的_colWidth、_rowHeight计算出首批渲染数据,再根据首次渲染的列表元素计算出真实的_colWidth和_rowHeight useEffect(() => { let timer = null let count = 0 if (boxRef.current) { if (list.length === 0) { setShowList([]) return } const { clientHeight, clientWidth } = boxRef.current calculateNum(_colWidth, _rowHeight, clientHeight, clientWidth) timer = setInterval(() => { count++ if (count > 5 || boxRef.current?.offsetParent === null) { // 父元素隐藏了立马停止计算 clearInterval(timer) return } calculateSize(Array.from(boxRef.current?.children[0]?.children || []), clientHeight, clientWidth) }, 200) } () => clearInterval(timer) }, [list]) // 计算滚动到底部时,最后一个元素超出可视区域的距离,弥补行列计算差异导致的展示不全 const beyondDistanceHandle = useCallback((children, scrollHeight) => { const len = children.length if (len > 0) { const lastChild = children[len - 1] const { height } = lastChild.getBoundingClientRect() const marginBottom = Number(getPropertyValue(lastChild, 'margin-bottom').slice(0, -2)) const top = lastChild.offsetTop + height + marginBottom const _beyondDistance = top - scrollHeight if (_beyondDistance > 0) { setBeyondDistance(_beyondDistance) } } }, []) // 滚动监听,根据滚动距离重新计算展示的数据 const onScroll = useCallback((e) => { const { scrollTop = 0, clientHeight = 0, scrollHeight = 0 } = e?.target || {} const rowNum = Math.floor(scrollTop / _rowHeight) // 滚动的行数 = 滚动距离 / 每一行的高度 let startIndex = rowNum if (hasMoreCol) startIndex *= colNum // 如果有多列,数据的开始索引 = 滚动行数 * 列数 const endIndex = num + startIndex // 结束索引 = 可视区域容纳数 + 新的开始索引 setPaddingTop(`${e.target.scrollTop}px`) // 设置列表paddingTop setData(startIndex, endIndex) // 更新渲染数据 if (scrollHeight - scrollTop === clientHeight && !beyondDistance) { // 滚动到底部 beyondDistanceHandle(Array.from(boxRef.current?.children[0]?.children || []), scrollHeight) } }, [colNum, num, hasMoreCol, setData, _rowHeight, beyondDistanceHandle, beyondDistance]) // 设置height和paddingTop,模拟滚动 const ulStyleStr = useMemo(() => ({ height: `${Math.ceil(list.length / colNum) * _rowHeight + beyondDistance}px`, minHeight: '100%', paddingTop }), [list, colNum, _rowHeight, paddingTop, beyondDistance]) return (
    {typeof ulRender === 'function' ? ( ulRender(showList, ulStyleStr) ) : (
      {showList.map((item) => ( typeof liRender === 'function' ? ( liRender(item) ) : (
    • {item}
    • ) ))}
    )}
    ) }

    index.less

    .virtualized {
      position: relative;
      overflow-y: auto;
      border: 1px solid #ddd;
      height: 200px;
    }
    
    

    你可能感兴趣的:(React实现虚拟滚动列表)