复杂场景:如何实现一个支持10万条数据的虚拟列表?
要在一个页面中流畅展示10万条数据,核心思路是“虚拟列表”(也称“虚拟滚动”)。它的原理很简单:只渲染用户当前能看到的那一小部分数据,而不是把10万条数据一次性全部生成DOM节点。
这样做能带来巨大的性能提升。有测试表明,直接渲染10万条数据,首次渲染可能耗时超过8秒,内存占用超过400MB;而使用虚拟列表,实际渲染的DOM节点数量能控制在50个以内,内存占用从MB级降至KB级。
🚀 核心实现步骤
无论你使用什么前端框架,实现一个基础的固定高度虚拟列表,都可以遵循以下5个步骤:
-
确定基础参数:你需要知道容器的高度 (
containerHeight)、每个列表项的高度 (itemHeight)、总数据量 (totalCount),以及一个可选的预渲染数量 (overscan,比如上下各多渲染2-5项,防止快速滚动时出现白屏)。 -
计算可见范围:监听容器的滚动事件,获取当前的滚动距离 (
scrollTop)。然后计算出当前应该显示哪些数据。startIndex = Math.floor(scrollTop / itemHeight)endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight) + overscan, totalCount)
-
创建“占位”元素:在列表容器内,你需要创建一个高度等于所有数据总高度 (
totalHeight = totalCount * itemHeight) 的“占位”元素(也叫“幽灵”元素)。它的作用是撑开滚动条,让浏览器认为列表非常长,从而可以正常滚动。 -
渲染可见项并定位:只渲染
startIndex到endIndex之间的数据。同时,用一个容器包裹这些可见项,并通过transform: translateY(startIndex * itemHeight)或top: startIndex * itemHeight的方式,将它们“推”到正确的位置上。 -
监听滚动事件:为容器绑定
scroll事件监听器。当滚动发生时,重新执行步骤2和步骤4,更新可见的数据。
📝 代码示例
这里分别提供React、Vue和原生JS的简化实现,你可以根据需要进行调整。
React 实现 (Hooks)
import React, { useState, useEffect, useRef } from 'react';
const VirtualList = ({ items, itemHeight, containerHeight }) => {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);
// 预渲染数量(缓冲区),防止快速滚动时出现白屏
const overscan = 5;
// 1. 计算可见范围
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
// 2. 计算占位元素总高度
const totalHeight = items.length * itemHeight;
// 3. 获取当前可见的数据
const visibleItems = items.slice(startIndex, endIndex);
// 4. 监听滚动事件
useEffect(() => {
const handleScroll = () => {
setScrollTop(containerRef.current.scrollTop);
};
const container = containerRef.current;
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, []);
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto', position: 'relative' }}
>
{/* 占位元素:撑开滚动条 */}
<div style={{ height: totalHeight, position: 'relative' }}>
{/* 内容容器:通过 transform 将可见项定位到正确位置 */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
transform: `translateY(${startIndex * itemHeight}px)`,
}}
>
{visibleItems.map((item, index) => (
<div key={item.id} style={{ height: itemHeight, ...item.style }}>
{item.content}
</div>
))}
</div>
</div>
</div>
);
};
export default VirtualList;
Vue 3 实现 (Composition API)
<template>
<div
ref="containerRef"
:style="{ height: containerHeight + 'px', overflow: 'auto', position: 'relative' }"
@scroll="handleScroll"
>
<!-- 占位元素 -->
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 内容容器 -->
<div
:style="{
position: 'absolute',
top: 0,
left: 0,
right: 0,
transform: `translateY(${startIndex * itemHeight}px)`,
}"
>
<div
v-for="(item, index) in visibleItems"
:key="item.id"
:style="{ height: itemHeight + 'px' }"
>
{{ item.content }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
items: Array,
itemHeight: Number,
containerHeight: Number,
});
const scrollTop = ref(0);
const containerRef = ref(null);
const overscan = 5;
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - overscan);
});
const endIndex = computed(() => {
return Math.min(
props.items.length,
Math.ceil((scrollTop.value + props.containerHeight) / props.itemHeight) + overscan
);
});
const totalHeight = computed(() => props.items.length * props.itemHeight);
const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value));
const handleScroll = (event) => {
scrollTop.value = event.target.scrollTop;
};
</script>
原生 JavaScript 实现
<!DOCTYPE html>
<html>
<head>
<style>
#list-container {
height: 600px;
overflow: auto;
position: relative;
border: 1px solid #ccc;
}
.list-item {
height: 50px; /* 固定高度 */
box-sizing: border-box;
padding: 10px;
border-bottom: 1px solid #eee;
}
</style>
</head>
<body>
<div id="list-container"></div>
<script>
// 1. 模拟10万条数据
const totalCount = 100000;
const items = Array.from({ length: totalCount }, (_, i) => ({
id: i,
content: `Item ${i}`
}));
const container = document.getElementById('list-container');
const itemHeight = 50;
const containerHeight = 600;
const overscan = 5;
// 2. 创建占位元素
const phantom = document.createElement('div');
phantom.style.height = `${items.length * itemHeight}px`;
phantom.style.position = 'relative';
container.appendChild(phantom);
// 3. 创建内容容器
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.top = '0';
content.style.left = '0';
content.style.right = '0';
phantom.appendChild(content);
function render() {
const scrollTop = container.scrollTop;
// 计算可见范围
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
// 定位内容容器
content.style.transform = `translateY(${startIndex * itemHeight}px)`;
// 清空并重新渲染可见项
content.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const item = items[i];
const div = document.createElement('div');
div.className = 'list-item';
div.textContent = item.content;
content.appendChild(div);
}
}
// 4. 监听滚动事件
container.addEventListener('scroll', render);
// 5. 首次渲染
render();
</script>
</body>
</html>
✨ 优化与进阶
- 处理动态高度:如果列表项高度不固定,事情会复杂一些。你需要先渲染一个不可见的项来测量其真实高度,并缓存起来。社区库如
react-window的VariableSizeList或vue-virtual-scroller都支持这个功能。 - 滚动事件优化:滚动事件触发非常频繁,可以使用
requestAnimationFrame或 节流(throttle) 函数来减少计算次数,提升性能。 - 使用
<span>标签:列表项内部的元素尽量使用<span>、<div>等通用标签,避免使用<table>等复杂布局,以减轻渲染压力。 - 使用
will-change:对即将发生变化的元素(如内容容器)应用will-change: transform;,可以提示浏览器进行GPU加速,提升滚动流畅度。 - 使用
Intersection Observer:这是一个更高效的API,可以用来替代scroll事件监听,由浏览器在元素进入视口时自动触发回调。 - 启用缓存:对于复杂列表项,可以对渲染结果进行缓存,避免在快速滚动时重复渲染。
📚 推荐使用的库
如果不想从零开始造轮子,可以直接使用社区成熟的库,它们已经帮你处理好了各种边缘情况。
| 框架 | 推荐库 | 说明 |
|---|---|---|
| React | react-window | 最推荐,轻量、高效,是 react-virtualized 的现代化重写,API更简洁。 |
react-virtualized | 功能更全面,但体积较大,对于10万条数据的需求,react-window 通常已足够。 | |
@tanstack/react-virtual | 无头UI库,提供强大的Hook,灵活性非常高。 | |
| Vue | vue-virtual-scroller | Vue官方推荐,功能强大,支持动态高度。 |
vue-virtual-scroll-list | 另一个流行的选择,配置简单。 | |
| 框架无关 | virtua | 轻量、高性能,可与React、Vue等配合使用。 |
react-window 的基本用法非常简单(以固定高度为例):
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style, data }) => (
<div style={style}>Row {index}</div>
);
const MyList = () => (
<List
height={600}
itemCount={100000}
itemSize={50}
width="100%"
>
{Row}
</List>
);
虚拟列表是处理海量数据渲染的标准方案。如果上述某个环节需要更具体的代码或解释,可以随时再问我。