1 使用react-window库
react-window 是一个用于优化渲染大型列表和表格的 React 库。它通过只渲染可见区域的元素来提高性能。
1.1 列表项的高度是固定的
import React from "react"
import { FixedSizeList as List } from "react-window"
const items = Array.from({ length: 1000 }, (_, index) =>
`Item ${index}`.repeat(20)
)
const Row = ({
index,
style,
}: {
index: number
style: React.CSSProperties
}) => <div style={style}>{items[index]}</div>
const App = () => (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
width: "100vw",
}}
>
<List
height={500} // 整个列表的高度
itemCount={items.length} // 项目总数
itemSize={60} // 每项的高度
width={600} // 列表的宽度
>
{Row}
</List>
</div>
)
export default App
1.2 列表项的高度是不固定的
import React, { useRef } from "react"
import { VariableSizeList as List } from "react-window"
const items = Array.from({ length: 1000 }, (_, index) =>
`Item ${index}`.repeat(20)
)
const Row = ({
index,
style,
}: {
index: number
style: React.CSSProperties
}) => <div style={style}>{items[index]}</div>
const App = () => {
const listRef = useRef<List>(null)
// 定义一个函数来获取每个项目的高度
const getItemSize = (index: number): number => {
// 可以根据项目的内容返回不同的高度
// 这里我们将每个偶数项的高度设置为50,奇数项设置为75
return index % 2 === 0 ? 50 : 75
}
const scrollTo = (index: number) => {
if (listRef.current) {
//VariableSizeList 组件的 scrollToItem 方法
listRef.current.scrollToItem(index)
}
}
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
width: "100vw",
}}
>
<button onClick={() => scrollTo(500)}>Scroll to Item 500</button>
<List
height={500} // 列表的高度
itemCount={items.length} // 项目总数
itemSize={getItemSize} // 获取每项的高度
width={600} // 列表的宽度
ref={listRef}
>
{Row}
</List>
</div>
)
}
export default App
2 自己实现虚拟列表
2.1 列表项的高度不固定
// App.txs
import React from "react"
import { VirtualList } from "./components/VirtualList"
const APP = () => {
const list = Array.from({ length: 1000 }, (_, index) =>
`Item ${index}`.repeat(20)
)
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
width: "100vw",
}}
>
<VirtualList list={list} containerHeight={550} bufferCount={1} />
</div>
)
}
// components/VirtualList/index.tsx
import React, { useState, useRef, useEffect } from "react"
interface VirtualListProps {
/** 列表项数组 */
list: string[]
/** 容器高度 */
containerHeight: number
/** 缓冲区数量 */
bufferCount: number
}
export const VirtualList: React.FC<VirtualListProps> = ({
list,
containerHeight,
bufferCount,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
/** 当前滚动位置 */
const [scrollTop, setScrollTop] = useState(0)
/** 每个列表项的高度数组 */
const [itemHeights, setItemHeights] = useState<number[]>([])
// 获取每项的高度
const getItemHeight = (index: number): number => {
// 偶数项高度为50,奇数项高度为75
return index % 2 === 0 ? 50 : 75
}
// 在组件挂载后计算每个列表项的高度
useEffect(() => {
const heights = list.map((_item, index) => getItemHeight(index))
setItemHeights(heights)
}, [list])
// 计算所有列表项的总高度
const docTotalHeight = itemHeights.reduce((sum, cur) => {
return sum + cur
}, 0)
// 计算当前视口内可见的列表项索引范围
const getVisibleItemsIndex = () => {
let accumulatedHeight = 0
let startIndex = 0
let endIndex = list.length - 1
// 计算开始索引
for (let i = 0; i < list.length; i++) {
accumulatedHeight += itemHeights[i]
if (accumulatedHeight > scrollTop) {
startIndex = i
break
}
}
accumulatedHeight = 0
//计算结束索引
for (let i = startIndex; i < list.length; i++) {
accumulatedHeight += itemHeights[i]
if (accumulatedHeight > containerHeight) {
endIndex = i
break
}
}
// 返回带有缓冲区的可见项索引范围
return {
startIndex: Math.max(startIndex - bufferCount, 0),
endIndex: Math.min(endIndex + bufferCount, list.length - 1),
}
}
// 根据当前滚动位置计算可见项的索引范围
const { startIndex, endIndex } = getVisibleItemsIndex()
// 处理滚动事件
const handleScroll = () => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop)
}
}
// 在组件挂载时添加滚动事件监听器,并在组件卸载时移除监听器
useEffect(() => {
const viewport = containerRef.current
if (viewport) {
viewport.addEventListener("scroll", handleScroll)
return () => {
viewport.removeEventListener("scroll", handleScroll)
}
}
}, [])
const renderItem = (item: string) => {
return <span>{item}</span>
}
// 用于存储渲染的列表项
const items = []
// 每个项的偏移量
let topOffset = 0
// 使用for循环得到开始项的偏移量
for (let i = 0; i < startIndex; i++) {
topOffset += itemHeights[i] || 0
}
// 根据计算的索引范围渲染可见项
for (let i = startIndex; i <= endIndex; i++) {
items.push(
<div
key={i}
style={{ position: "absolute", top: `${topOffset}px`, width: "100%" }}
>
{renderItem(list[i])}
</div>
)
topOffset += itemHeights[i] || 0
}
return (
<div
ref={containerRef}
style={{
height: containerHeight,
width: "80%",
overflowY: "auto",
position: "relative",
border: "1px solid #ccc",
}}
>
<div style={{ height: `${docTotalHeight}px`, position: "relative" }}>
{items}
</div>
</div>
)
}
2.2 列表项的高度不清楚
列表项的高度由其内容撑开,因此这个时候我们不能提前知道每项的高度。
// App.txs
import React from "react"
import { VirtualList } from "./components/VirtualList"
const APP = () => {
const list = Array.from({ length: 1000 }, (_, index) =>
`Item ${index}`.repeat(index + 1)
)
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
width: "100vw",
}}
>
<VirtualList list={list} containerHeight={550} bufferCount={1} />
</div>
)
}
// components/VirtualList/index.tsx
import React, { useState, useRef, useEffect } from "react"
interface VirtualListProps {
/** 列表项数组 */
list: string[]
/** 容器高度 */
containerHeight: number
/** 缓冲区数量 */
bufferCount: number
}
export const VirtualList: React.FC<VirtualListProps> = ({
list,
containerHeight,
bufferCount,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
/** 当前滚动位置 */
const [scrollTop, setScrollTop] = useState(0)
/** 每个列表项的高度数组 */
const [itemHeights, setItemHeights] = useState<number[]>([])
// 在组件挂载后计算每个列表项的高度
useEffect(() => {
// 使用每项dom实例的offsetHeight获取该项的高度*************************与2.1的区别在这里
const heights = itemRefs?.current?.map((ref) =>
ref ? ref.offsetHeight : 0
)
setItemHeights(heights)
}, [list])
// 计算所有列表项的总高度
const docTotalHeight = itemHeights.reduce((sum, cur) => {
return sum + cur
}, 0)
// 计算当前视口内可见的列表项索引范围
const getVisibleItemsIndex = () => {
let accumulatedHeight = 0
let startIndex = 0
let endIndex = list.length - 1
// 计算开始索引
for (let i = 0; i < list.length; i++) {
accumulatedHeight += itemHeights[i]
if (accumulatedHeight > scrollTop) {
startIndex = i
break
}
}
accumulatedHeight = 0
//计算结束索引
for (let i = startIndex; i < list.length; i++) {
accumulatedHeight += itemHeights[i]
if (accumulatedHeight > containerHeight) {
endIndex = i
break
}
}
// 返回带有缓冲区的可见项索引范围
return {
startIndex: Math.max(startIndex - bufferCount, 0),
endIndex: Math.min(endIndex + bufferCount, list.length - 1),
}
}
// 根据当前滚动位置计算可见项的索引范围
const { startIndex, endIndex } = getVisibleItemsIndex()
// 处理滚动事件
const handleScroll = () => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop)
}
}
// 在组件挂载时添加滚动事件监听器,并在组件卸载时移除监听器
useEffect(() => {
const viewport = containerRef.current
if (viewport) {
viewport.addEventListener("scroll", handleScroll)
return () => {
viewport.removeEventListener("scroll", handleScroll)
}
}
}, [])
const renderItem = (item: string) => {
return <span>{item}</span>
}
// 用于存储渲染的列表项
const items = []
// 每个项的偏移量
let topOffset = 0
// 使用for循环得到开始项的偏移量
for (let i = 0; i < startIndex; i++) {
topOffset += itemHeights[i] || 0
}
// 根据计算的索引范围渲染可见项
for (let i = startIndex; i <= endIndex; i++) {
items.push(
<div
key={i}
ref={(el) => {
itemRefs.current[i] = el
}}
style={{ position: "absolute", top: `${topOffset}px`, width: "100%" }}
>
{renderItem(list[i])}
</div>
)
topOffset += itemHeights[i] || 0
}
return (
<div
ref={containerRef}
style={{
height: containerHeight,
width: "80%",
overflowY: "auto",
position: "relative",
border: "1px solid #ccc",
}}
>
<div style={{ height: `${docTotalHeight}px`, position: "relative" }}>
{items}
</div>
</div>
)
}