一看就懂的虚拟列表

前言

在前端开发过程中,有时候会遇到一些不能使用分页来加载数据的情况,因此当我们需要渲染上十万条数据的时候,可能会造成渲染的卡顿,导致用户体验特别不好,那么对于这种情况我们该怎么去解决呢?这个时候就不得不提到虚拟列表

什么是虚拟列表

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为550px,而列表项的高度为55px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可

为啥需要使用虚拟列表

虚拟列表可以解决一次性渲染数据量过大时,页面卡顿,(比如: table不分页并且一次性加载上万条复杂的数据)

成熟的虚拟列表方案

Ant-design — 虚拟列表

但是这些成熟的方案不一定能满足你的需求(比如:antd官网这个案列不支持hover时候行高亮),因此,我们需要知道这个虚拟列表的实现原理,以便在以后的工作中,随机应变,举一反三

自己实现
原理图

在这里插入图片描述

实现步骤
  1. 外层容器高度固定(550),并且设置(overflowY: ‘auto’)

  2. 计算内层容器放下全部数据的高度(total * 55)

  3. 计算在这个固定容器高度下可视区可以装多少条数据(limit = 550 / 55)

  4. 滚动的时候,通过参数scrollTop,可以计算可视区展示的第一条数据是第几条(startIndex = scrollTop / 55 )和最后一条数据是第几条(endIndex = limit + startIndex)

  5. 优化,加一个缓冲条数bufferSize,防止鼠标滚动过快的时候出现空白区域

  • 固定外层容器高度

    <div
    style={{
           margin: '0 auto',
           height: '550px',
           width: '100%',
           overflowY: 'auto',
           msOverflowX: 'hidden',
           // 为了直观点这里用红色表示外层容器高度
           border:'1px solid red',
          }}
    onScroll={onScroll}
    ref={ref}
    >
          <div>{/* 内存容器 */}</div>
    </div>
    
    
  • 向容器中添加一层内层容器,并计算在总数据下,内层容器被撑开的最大高度

    <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' }}>
    		 {/* 大量的数据 */}
    	</div>
    </div>
    
    
  • 计算在外层容器高度范围内可见的数据条数

 limit = 550(外层容器的height) / 55(每一条数据的行高)
  • 计算可视区第一条和最后一条数据分别在总数据的第几条
const limit = 10;
let originStartIdx = 0;
// total是总共有多少条数据
const total = 100000;
// rowHeight是每一条数据的行高
const rowHeight = 55;

const vTable = () => {
  const [startIndex, setStart] = useState(Math.max(originStartIdx, 0));
  const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit, total - 1));
  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, 0))
                  setEnd(Math.min(currIndex + limit, total - 1))
              }
          }
  }
}

这样滑动过快会出现空白区域
在这里插入图片描述

  • 添加缓冲参数,防止滚动过快出现空白
const limit = 10;
let originStartIdx = 0;
// total是总共有多少条数据
const total = 100000;
// rowHeight是每一条数据的行高
const rowHeight = 55;
// 用于缓冲(防止滚动过快,出现白屏)
const bufferSize = 20;

const vTable = () => {
  const [startIndex, setStart] = useState(Math.max(originStartIdx - bufferSize, 0));
  const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit + bufferSize, total - 1));
  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))
              }
          }
  }
}

添加后滑动过快空白消失
在这里插入图片描述

  • 全部代码

    import { useState, useRef } from "react";
    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;
    
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值