列表虚拟滚动

虚拟列表的核心就是用户滚动时只渲染可视区域的元素,不可视区域使用空盒子高度撑起来用来体现滚动效果。

首先我们知道单个元素的高度、可视区域的高度、多渲染数量用于处理一些可见但不完全可见的元素。

 const expendCount = 4; // 多渲染数量,用于处理一些可见但不完全可见的元素
 const screenHeight = 560; // 可视区域的高度
 const renderItemHeight = 35; // 每条数据的固定高度

使用Ref绑定可滚动的盒子,将高度设置成可视区域的高度,并绑定滚动事件用于处理元素滚动时的数据变化。


import React from 'react';
import './index.less';

export default function Index() {
    const expendCount = 4; // 多渲染数量,用于处理一些可见但不完全可见的元素
    const screenHeight = 560; // 可视区域的高度
    const renderItemHeight = 35; // 每条数据的固定高度
    // 滚动容器Ref
    const scrollBoxRef = useRef(null);
     // 滚动监听
    const virtualBoxScroll = () => {
       ...
    };

    return (
        <div className="virtual-wrap">
            {/* 外层盒子 屏幕高度 可滚动*/}
            <div
                className="virtual-scroll-wrap"
                ref={scrollBoxRef}
                style={{ height: `${screenHeight}px` }}
                onScroll={virtualBoxScroll}
            >
            </div>
        </div>
    );
}

css样式:将外层盒子(屏幕高度盒子设定位overflow-y: auto;)用于滚动

.virtual-wrap {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: black;
    height: 100vh;
    overflow: hidden;
    box-sizing: border-box;
    border: 1px solid pink;

    .virtual-scroll-wrap {
        width: 300px;
        border: 4px solid skyblue;
        overflow-y: auto;
        position: relative;
        background-color: azure;
    }
}

设置一个空盒子用于撑开外部盒子的高度,高度设定为每项数据高度的总和。


import React from 'react';
import './index.less';

export default function Index() {
    //列表数据
    const data = new Array(1000).fill(0).map((_, index) => `${index + 1}`);
    const expendCount = 4; // 多渲染数量,用于处理一些可见但不完全可见的元素
    const screenHeight = 560; // 可视区域的高度
    const renderItemHeight = 35; // 每条数据的固定高度
    // 滚动容器Ref
    const scrollBoxRef = useRef(null);
     // 滚动监听
    const virtualBoxScroll = () => {
       ...
    };

    return (
        <div className="virtual-wrap">
            {/* 外层盒子 屏幕高度 可滚动*/}
            <div
                className="virtual-scroll-wrap"
                ref={scrollBoxRef}
                style={{ height: `${screenHeight}px` }}
                onScroll={virtualBoxScroll}
            >
                 {/* 用于撑起外部盒子的高度*/}
                <div
                    className="scroll-box"
                    style={{ height: `${data.length * renderItemHeight}px` }}
                ></div>
            </div>
        </div>
    );
}

列表盒子用于显示实际渲染条目数,style属性设定为( transform: `translateY'),translateY的参数设为初始元素索引*每条数据的固定高度


import React from 'react';
import './index.less';

export default function Index() {
    // 开始,结束渲染索引
    const [endIndex, setEndIndex] = useState(20);
    const [startIndex, setStartIndex] = useState(0);
    //列表数据
    const data = new Array(1000).fill(0).map((_, index) => `${index + 1}`);
    const expendCount = 4; // 多渲染数量,用于处理一些可见但不完全可见的元素
    const screenHeight = 560; // 可视区域的高度
    const renderItemHeight = 35; // 每条数据的固定高度
    // 滚动容器Ref
    const scrollBoxRef = useRef(null);
     // 滚动监听
    const virtualBoxScroll = () => {
       ...
    };

    return (
        <div className="virtual-wrap">
            {/* 外层盒子 屏幕高度 可滚动*/}
            <div
                className="virtual-scroll-wrap"
                ref={scrollBoxRef}
                style={{ height: `${screenHeight}px` }}
                onScroll={virtualBoxScroll}
            >
                 {/* 用于撑起外部盒子的高度*/}
                <div
                    className="scroll-box"
                    style={{ height: `${data.length * renderItemHeight}px` }}
                ></div>
                 {/*  实际渲染条目 */}
                <div
                    className="render-box"
                    style={{
                        transform: `translateY(${startIndex * renderItemHeight}px)`,
                    }}
                >
        
                </div>
            </div>
        </div>
    );
}

css样式:

 .render-box {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
 }

渲染列表数据,列表渲染数据从总数据中用开始元素索引到结束元素的索引进行截取

// 渲染列表
const renderList = useMemo(
   () => data.slice(startIndex, endIndex),
   [startIndex, endIndex]
); 

{/* 实际渲染条目 */}
<div
    className="render-box"
    style={{
        transform: `translateY(${startIndex * renderItemHeight}px)`,
    }}
>
    {renderList?.map((item) => (
        <div
            className="scroll-item"
            style={{ height: `${renderItemHeight}px` }}
            key={item}
        >
            <span></span>
            {item}
        </div>
    ))}
</div>

元素滚动时触发滚动事件 ,计算滚动的距离,用滚动距离/每条元素的高度并向下取整得出新的开始元素索引,用新开始元素索引+可视高度/每项元素高度并向上取整+多渲染条数得出新结束元素的索引,将新的开始结束索引更新到State

// 滚动监听
const virtualBoxScroll = () => {
    // 滑动距离
    const scrollDistance = scrollBoxRef.current.scrollTop;
    // 计算新索引
    const startIndex = Math.floor(scrollDistance / renderItemHeight);
    const endIndex =
        startIndex + Math.ceil(screenHeight / renderItemHeight) + expendCount; // 多渲染5条
    // 更新索引
    setStartIndex(startIndex);
    setEndIndex(endIndex);
};

全部代码:

import React, { useState, useRef, useMemo } from 'react';
import './App.less';

export default function Index() {
    const expendCount = 4; // 多渲染数量,用于处理一些可见但不完全可见的元素
    const screenHeight = 560; // 渲染屏幕高度
    const renderItemHeight = 35; // 每条数据的固定高度

    // 滚动容器Ref
    const scrollBoxRef = useRef(null);
    // 数据构造
    const data = new Array(1000).fill(0).map((_, index) => `${index + 1}`);

    // 开始,结束渲染索引
    const [endIndex, setEndIndex] = useState(20);
    const [startIndex, setStartIndex] = useState(0);
    // 渲染列表
    const renderList = useMemo(
        () => data.slice(startIndex, endIndex),
        [startIndex, endIndex]
    );

    // 滚动监听
    const virtualBoxScroll = () => {
        // 滑动距离
        const scrollDistance = scrollBoxRef.current.scrollTop;
        // 计算新索引
        const startIndex = Math.floor(scrollDistance / renderItemHeight);
        const endIndex =
            startIndex + Math.ceil(screenHeight / renderItemHeight) + expendCount; // 多渲染5条
        // 更新索引
        setStartIndex(startIndex);
        setEndIndex(endIndex);
    };

    return (
        <div className="virtual-wrap">
            {/* 外层盒子 屏幕高度 可滚动*/}
            <div
                className="virtual-scroll-wrap"
                ref={scrollBoxRef}
                style={{ height: `${screenHeight}px` }}
                onScroll={virtualBoxScroll}
            >
                {/* 滚动盒子 渲染高度 空盒子 撑起高度*/}
                <div
                    className="scroll-box"
                    style={{ height: `${data.length * renderItemHeight}px` }}
                ></div>

                {/*  实际渲染条目 */}
                <div
                    className="render-box"
                    style={{
                        transform: `translateY(${startIndex * renderItemHeight}px)`,
                    }}
                >
                    {renderList?.map((item) => (
                        <div
                            className="scroll-item"
                            style={{ height: `${renderItemHeight}px` }}
                            key={item}
                        >
                            <span></span>
                            {item}
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}

css:

.virtual-wrap {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: black;
    height: 100vh;
    overflow: hidden;
    box-sizing: border-box;
    border: 1px solid pink;

    .virtual-scroll-wrap {
        width: 300px;
        border: 4px solid skyblue;
        overflow-y: auto;
        position: relative;
        background-color: azure;

        .render-box {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
        }

        .scroll-item {
            display: flex;
            align-items: center;
            box-sizing: border-box;
            padding: 0 20px;
            color: #2b2b2b;
            border-bottom: 1px solid #2b2b2b;
            background-color: azure;
            box-shadow: 0 0 4px #ccc;

            span {
                display: inline-block;
                margin-right: 15px;
                border-radius: 50%;
                height: 16px;
                width: 16px;
                background-color: pink;
            }
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值