前端之虚拟列表

112 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

虚拟列表定义

虚拟列表其实就是当我们前端页面渲染大量数据的时候,如果一次性渲染出来,一是非常耗费浏览器性能,造成页面卡顿,甚至崩溃,二是用户和体验非常不好,虚拟列表就是在我们的可视区域进行数据的渲染,当滚动到后面数据的时候再进行加载下面的数据,达到分批次渲染的目的

虚拟列表的原理和实现

原理图

捕获.PNG

实现的步骤

  1. 外层容器高度固定(550),并且设置(overflowY: ‘auto’)
  2. 计算内层容器放下全部数据的高度(total * 55)
  3. 计算在这个固定容器高度下可视区可以装多少条数据(limit = 550 / 55)
  4. 滚动的时候,通过参数scrollTop,可以计算可视区展示的第一条数据是第几条(startIndex = scrollTop / 55 )和最后一条数据是第几条(endIndex = limit + startIndex)
  5. 优化,加一个缓冲条数bufferSize,防止鼠标滚动过快的时候出现空白区域
import { select } from "@querycap-ui/core";
const total = 100000;
const rowHeight = 55;
// 用于缓冲(防止滚动过快,出现白屏)
const bufferSize = 20;
const limit = 10;
let originStartIdx = 0;
const getdata = () => {
    const data = [];
    for (let i = 0; i < total; i++) {
        data.push({
            title: `标题${i}`,
            age: `年纪${i}`,
            address: `地址${i}`,
        })
    }
    return data;
}
const data = getdata();
const Home = () => {
    const [current, setCurrent] = useState<undefined | number>();
    const [startIndex, setStart] = useState(Math.max(originStartIdx - bufferSize, 0));
    const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit + bufferSize, total - 1));
    const ref = useRef<HTMLDivElement>(null);
    const onScroll = (e: any) => {
        if (e.target === ref.current) {
            const { scrollTop } = e.target;
            // scrollTop 获取到的是被滚动元素在垂直位置滚动的距离。
            const currIndex = Math.floor(scrollTop / rowHeight);
            if (originStartIdx !== currIndex) {
                originStartIdx = currIndex;
                setStart(Math.max(currIndex - bufferSize, 0))
                setEnd(Math.min(currIndex + limit + bufferSize, total - 1))
            }
        }
    }

    const edite = (data: any, index: number) => {
        setCurrent(index);
        console.log('编辑了这一条数据---->',data)
    }

    const rowRenderer = (obj: any) => {
        const { index, style } = obj;
        return (
            <div 
                key={index}
                style={style}
                css={
                    select().backgroundColor(index === current ? '#dcebf8' : '').display('flex')
                        .with(select(':hover').backgroundColor(index === current ? '#dcebf8' : '#fafafa'))
                        .with(select('>*').flex(1).padding('0px 16px'))
                }
            >
                <span>{data[index].title}</span>
                <span>{data[index].age}</span>
                <span>{data[index].address}</span>
                <a onClick={() => edite(data[index], index)}>编辑</a>
            </div>
        )
    }

    // 可视区域的数据
    const pushData = () => {
        const content = [];
        for (let i = startIndex; i <= endIndex; ++i) {
            content.push(
                rowRenderer({
                    index: i,
                    style: {
                        height: `${rowHeight}px`,
                        lineHeight: `${rowHeight}px`,
                        left: 0,
                        right: 0,
                        position: "absolute",
                        top: i * rowHeight,
                        borderBottom: "1px solid #f0f0f0",
                        width: "100%",
                        cursor: 'pointer',
                    }
                })
            );
        }
        return content;
    }

    return (
        <div
            style={{
                margin: '0 auto',
                height: '550px',
                width: '100%',
                overflowY: 'auto',
                msOverflowX: 'hidden',
                border:'1px solid red',
            }}
            onScroll={onScroll}
            ref={ref}
        >
            <div style={{ height: `${total * 55}px`, position: 'relative' }}>
                {pushData()}
            </div>
        </div>

    );
};

export default Home;