什么是虚拟列表?如何实现?
虚拟列表只渲染可视区域内的元素,而非全部,适用于大数据列表。
原理
容器高度: 600px
每项高度: 50px
可见项数: 600 / 50 = 12
总数据: 10000 条
实际渲染: ~15 条(12 + 上下缓冲)
┌──────────────────┐
│ ↑ 偏移量(padding)│
├──────────────────┤
│ Item 3 │ ← 可见区域
│ Item 4 │
│ Item 5 │
│ ... │
│ Item 14 │ ← 可见区域
├──────────────────┤
│ ↓ 偏移量(padding)│
└──────────────────┘实现核心
import { useState, useRef, useEffect, useMemo } from 'react'
function VirtualList({ items, itemHeight, visibleHeight }) {
const [scrollTop, setScrollTop] = useState(0)
const containerRef = useRef(null)
// 可见项数量
const visibleCount = Math.ceil(visibleHeight / itemHeight)
// 开始索引(加上上方缓冲)
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 3)
// 结束索引(加上下方缓冲)
const endIndex = Math.min(
items.length,
startIndex + visibleCount + 6
)
// 偏移量
const offsetY = startIndex * itemHeight
// 总高度
const totalHeight = items.length * itemHeight
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop)
}
return (
<div
ref={containerRef}
style={{ height: visibleHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{items.slice(startIndex, endIndex).map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
</div>
)
}动态高度的虚拟列表
当每项高度不固定时,需要维护位置缓存:
function DynamicVirtualList({ items, estimatedHeight }) {
const [positions, setPositions] = useState(
items.map((_, i) => ({
index: i,
top: i * estimatedHeight,
bottom: (i + 1) * estimatedHeight,
height: estimatedHeight
}))
)
// 测量真实高度后更新 positions
const updateHeight = (index, height) => {
setPositions(prev => {
const newPositions = [...prev]
const oldHeight = newPositions[index].height
const diff = height - oldHeight
newPositions[index].height = height
newPositions[index].bottom = newPositions[index].top + height
// 更新后续所有位置
for (let i = index + 1; i < newPositions.length; i++) {
newPositions[i].top = newPositions[i - 1].bottom
newPositions[i].bottom = newPositions[i].top + newPositions[i].height
}
return newPositions
})
}
// 二分查找 startIndex
const findIndex = (scrollTop) => {
let start = 0, end = positions.length - 1
while (start < end) {
const mid = Math.floor((start + end) / 2)
if (positions[mid].bottom < scrollTop) start = mid + 1
else end = mid
}
return start
}
}现成库
// react-window(推荐)
import { FixedSizeList, VariableSizeList } from 'react-window'
// 固定高度
<FixedSizeList height={600} itemCount={10000} itemSize={50} width="100%">
{({ index, style }) => (
<div style={style}>Item {index}</div>
)}
</FixedSizeList>
// 动态高度
<VariableSizeList height={600} itemCount={10000}
itemSize={i => items[i].height} width="100%">
{({ index, style }) => <div style={style}>Item {index}</div>}
</VariableSizeList>
// Vue: vue-virtual-scroller
// Angular: @angular/cdk/scrolling适用场景
- 万级数据列表
- 聊天记录
- 表格数据
- 时间线/日志