虚拟滚动列表高度计算不准怎么办?

极客婷婷 阅读 3

我用 React 实现了一个虚拟滚动列表,但 item 的高度是动态的(有的带图有的不带),结果滚动时内容错位、空白或者重叠,特别乱。

试过给每个 item 固定 height: 100px 能正常滚动,但实际项目里不能这么搞。有没有办法在保持动态高度的同时让虚拟滚动准确计算位置?

const getItemHeight = (index) => {
  // 这里其实没法提前知道真实渲染后的高度
  return data[index].hasImage ? 120 : 60;
};
我来解答 赞 0 收藏
二维码
手机扫码查看
1 条解答
开发者雨辰
这个问题出在你试图在 DOM 渲染之前“预知”高度。对于动态内容(特别是图片),布局是会变的,你写的那个静态估算函数一旦跟真实 DOM 高度对不上,错位是必然的。

要解决不定高度的虚拟滚动,核心思路只有一种:测量并缓存。不要瞎猜,先给个初始估算值渲染,等 item 真正挂载到页面上后,用 ResizeObserver 测出真实高度,然后回写到你的位置记录里,再重新计算总高度。

你可以试试下面这个方案,核心在于维护一个 positions 数组,利用 ResizeObserver 动态更新它。

import React, { useState, useRef, useEffect, useMemo } from 'react';

const DynamicVirtualList = ({ data, estimatedItemHeight = 60 }) => {
const [scrollOffset, setScrollOffset] = useState(0);
const containerRef = useRef(null);

// 缓存每个 item 的位置信息 { top, bottom, height, measurement }
// measurement 标记是否已经测量过真实高度
const [positions, setPositions] = useState(() =>
data.map((_, index) => ({
index,
top: index * estimatedItemHeight,
bottom: (index + 1) * estimatedItemHeight,
height: estimatedItemHeight,
measurement: false
}))
);

// 更新某个位置的高度信息
const updatePosition = (index, height) => {
setPositions((prev) => {
const newPositions = [...prev];
// 只有当高度确实变化了才更新,避免死循环
if (newPositions[index].height !== height) {
const oldHeight = newPositions[index].height;
const dHeight = height - oldHeight;

newPositions[index] = { ...newPositions[index], height, measurement: true };

// 更新后面所有 item 的 top/bottom
for (let i = index + 1; i < newPositions.length; i++) {
newPositions[i] = {
...newPositions[i],
top: newPositions[i].top + dHeight,
bottom: newPositions[i].bottom + dHeight
};
}
}
return newPositions;
});
};

// 监听滚动
const handleScroll = (e) => {
setScrollOffset(e.target.scrollTop);
};

// 计算可视区域的 startIndex 和 endIndex
const { visibleData, totalHeight, offsetY } = useMemo(() => {
const containerHeight = containerRef.current?.clientHeight || 0;
const startNode = positions.findIndex((p) => p.bottom > scrollOffset);
const endNode = positions.findIndex((p) => p.top > scrollOffset + containerHeight);

// 扩大渲染范围防止白屏
const startIndex = Math.max(0, startNode - 2);
const endIndex = Math.min(positions.length - 1, endNode === -1 ? positions.length - 1 : endNode + 2);

const totalHeight = positions[positions.length - 1]?.bottom || 0;
const offsetY = positions[startIndex]?.top || 0;

return {
visibleData: data.slice(startIndex, endIndex + 1).map((item, i) => ({
...item,
index: startIndex + i
})),
totalHeight,
offsetY
};
}, [scrollOffset, positions, data]);

return (
ref={containerRef}
onScroll={handleScroll}
style={{ height: '500px', overflow: 'auto', border: '1px solid #ccc' }}
>

{visibleData.map((item) => {
const pos = positions[item.index];
return (
key={item.id}
ref={(node) => {
if (node && !pos.measurement) {
// 使用 ResizeObserver 监听尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { height } = entry.contentRect;
updatePosition(item.index, height);
}
});
resizeObserver.observe(node);
// 清理 observer 防止内存泄漏(实际项目中建议用 useCallback + ref 存储)
return () => resizeObserver.disconnect();
}
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: translateY(${pos.top}px),
// 调试看看,加上背景色更容易看清错位
borderBottom: '1px solid #eee'
}}
>
{/* 你的动态内容 */}

{item.hasImage && img}
{item.content}



);
})}


);
};


这段代码的关键点在于 updatePosition 函数。一旦某个 item 的真实高度被测量出来(比如图片加载撑开了高度),它不仅更新当前 item 的高度,还会计算差值,把后面所有 item 的 top 和 bottom 都往后推,这样就保证了对齐。调试的时候你可以给 item 加个背景色,滚动起来看一眼就明白了。
点赞
2026-03-04 15:41