虚拟列表的核心就是用户滚动时只渲染可视区域的元素,不可视区域使用空盒子高度撑起来用来体现滚动效果。
首先我们知道单个元素的高度、可视区域的高度、多渲染数量用于处理一些可见但不完全可见的元素。
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;
}
}
}
}