胖蔡说技术
随便扯扯

前端虚拟列表的实现

前言

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

什么是虚拟列表 

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

为啥需要使用虚拟列表 

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

成熟的虚拟列表方案 

Ant-design — 虚拟列表

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

自己实现 

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

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

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

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

优化,加一个缓冲条数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(每一条数据的行高)
1
计算可视区第一条和最后一条数据分别在总数据的第几条
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))
              }
          }
  }
}

添加后滑动过快空白消失

全部代码

```go
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>

    );
};
赞(0) 打赏
转载请附上原文出处链接:胖蔡说技术 » 前端虚拟列表的实现
分享到: 更多 (0)

请小编喝杯咖啡~

支付宝扫一扫打赏

微信扫一扫打赏