在antd-mobile中使用Virtual List的实现与坑点

作为前端开发者,我们经常需要处理长列表数据的展示,而性能优化是其中关键的一环。本文将分享我在使用 antd-mobile 时整合虚拟列表(Virtual List)的实践经历,特别是遇到的坑和解决方案。

效果展示:

为什么需要 Virtual List

传统列表渲染会一次性渲染所有元素,当数据量很大时,会导致:

  1. 页面加载缓慢 2.内存占用高
  2. 滚动卡顿
  3. DOM 节点过多影响性能

虚拟列表通过仅渲染可视区域内的元素来解决这些问题,显著提升性能。

技术选型

尝试 react-virtuoso

我的第一个尝试是使用 react-virtuoso,一个现代化的虚拟列表库:

1
2
3
import { Virtuoso } from "react-virtuoso";

<Virtuoso data={data} itemContent={(index) => <Item item={data[index]} />} />;

遇到的坑点:当与 antd-mobile 的PullToRefresh组件结合使用时,发现下拉刷新会”吞噬”列表的滚动事件,导致列表无法滚动。这可能是因为两者都在尝试控制滚动行为,产生了冲突。

切换到 react-virtualized

由于上述问题,我转向了更成熟的 react-virtualized 库,它提供了 List 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {
List as VirtualList,
AutoSizer,
WindowScroller,
} from "react-virtualized";

<WindowScroller>
{({ height, scrollTop, isScrolling }) => (
<VirtualList
autoHeight
height={height}
width={width}
rowCount={data.length}
rowHeight={120}
rowRenderer={rowRender}
scrollTop={scrollTop}
isScrolling={isScrolling}
/>
)}
</WindowScroller>;

关键实现点

配合 antd-mobile 的 PullToRefresh

要实现下拉刷新,需要将PullToRefresh包裹在虚拟列表外面:

1
2
3
<PullToRefresh onRefresh={handleRefresh}>
<VirtualList /* 属性 */ />
</PullToRefresh>

绝对定位的 Items

虚拟列表的核心在于高效重用 DOM 元素。在使用 react-virtualised 时,必须为每个列表项设置绝对定位:

1
2
3
4
.list-item {
position: absolute;
top: /* ItemHeight * index */ ;
}

踩坑经历:如果不使用绝对定位,当组件卸载时,高度会变小,导致后续项目渲染到屏幕之外,出现”跳跃”问题。

窗口滚动器(WindowScroller)

为了与页面其他元素协调滚动,需要使用 WindowScroller

1
2
3
4
5
<WindowScroller scrollElement={scrollElement}>
{({ height, scrollTop, isScrolling }) => (
/* 虚拟列表 */
)}
</WindowScroller>

完整代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { InfiniteScroll, PullToRefresh } from "antd-mobile";
import {
AutoSizer,
List as VirtualList,
WindowScroller,
} from "react-virtualized";

export const ListComponent = () => {
const scrollElm = useRef(null);
const [element, setElement] = useState();

useEffect(() => {
if (scrollElm.current) {
setElement(scrollElm.current);
}
}, [scrollElm.current]);

const rowRender = ({ index }) => {
const item = data[index];
return (
<div
key={item.id}
style={{
position: "absolute",
top: `${index * 120}px`,
left: 0,
width: "100%",
height: "120px",
}}
>
<Item data={item} />
</div>
);
};

return (
<div className="container" ref={scrollElm}>
<WindowScroller scrollElement={element}>
{({ height, scrollTop, isScrolling }) => (
<PullToRefresh onRefresh={handleRefresh}>
<AutoSizer disableHeight>
{({ width }) => (
<VirtualList
autoHeight
width={width}
height={height}
rowCount={data.length}
rowHeight={120}
rowRenderer={rowRender}
overscanRowCount={5}
isScrolling={isScrolling}
scrollTop={scrollTop}
/>
)}
</AutoSizer>
</PullToRefresh>
)}
</WindowScroller>
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />
</div>
);
};

性能优化建议

  1. 批处理数据加载:结合InfiniteScroll实现分页加载
  2. 固定高度元素:为虚拟列表中的元素使用固定高度
  3. 合理设置 overscan:避免渲染过多不可见元素
  4. 记忆化行渲染:使用React.memouseMemo优化行组件