Vue3 虚拟列表实现原理与实战

前言

在日常开发中,我们经常会遇到需要展示大量数据的场景,比如展示几千条甚至上万条数据列表。如果直接将所有数据渲染到页面中,会导致严重的性能问题,页面会变得卡顿甚至崩溃。虚拟列表(Virtual List)正是为了解决这一问题而诞生的技术。

本文将介绍虚拟列表的核心原理,并基于 Vue3 手写一个完整的虚拟列表组件。


一、什么是虚拟列表

虚拟列表是一种按需渲染的技术,它只渲染当前可视区域内的列表项,而不是一次性渲染所有数据。想象一下,你有一本1000页的书,但你在任何时候只能看到其中的一页。虚拟列表的工作方式就和翻书一样------只展示当前"看得到"的内容。

虚拟列表的核心优势

优势 说明
性能提升 只渲染可见区域的数据,DOM 节点数量大幅减少
内存优化 避免创建大量 DOM 节点,降低内存占用
流畅体验 滚动更加流畅,不会因为数据量大而卡顿
快速首屏 首屏渲染时间大大缩短

二、虚拟列表的实现原理

虚拟列表的核心思想其实很简单,可以用以下几个步骤来概括:

  1. 计算可视区域:根据滚动位置和容器高度,计算出当前可见的数据范围
  2. 按需渲染:只渲染计算出的可见数据,而不是全部数据
  3. 占位模拟:通过 padding 或 transform 模拟完整的滚动高度,让滚动条看起来正常

下面这张图清晰地展示了虚拟列表的工作原理:

复制代码
┌─────────────────────────────┐
│  已渲染区域(可见)           │
│  ┌─────────────────────┐     │
│  │ Item 3              │     │
│  │ Item 4              │     │
│  │ Item 5              │     │ ← 当前可见区域
│  │ Item 6              │     │
│  │ Item 7              │     │
│  └─────────────────────┘     │
├─────────────────────────────┤
│  未渲染区域(不可见)         │
│  ... Item 8-100              │
└─────────────────────────────┘

三、代码实现

子组件:MyList.vue

vue 复制代码
<template>
    <div style="overflow-y:auto ;border:3px solid blue;position: relative;" :style="{ height:props.size * 10 + 'px'}" @scroll="handSrool">
        <ul :style="{ height:(props.size+1) * props.testData.length + 'px', transform: `translateY(${offsetY}px)` }" >
            <li v-for="(item, index) in list" class="list-item" :style="{ height:props.size + 'px', position: 'absolute', top: (index * props.size) + 'px', width: '100%' }">
                {{ item }}
            </li>
        </ul>
    </div>
</template>

<script setup>
import { onMounted, ref, computed } from 'vue';

// 接受父节点参数
const props = defineProps({
    testData: Array,
    size: {
        type: Number,
        default: '6'
    }
})

const startIndex = ref(0)
const endIndex = computed(() => startIndex.value + 10)

// 计算偏移量,让列表项跟随滚动
const offsetY = computed(() => startIndex.value * props.size)

const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))

// 监听滚动
const handSrool = (e) => {
    // 获取滚动距离
    const scrollTop = e.target.scrollTop
    // 根据滚动距离计算起始索引
    startIndex.value = Math.floor(scrollTop / props.size)
}

onMounted(() => {
    console.log(props.size)
    console.log(endIndex.value)
})
</script>

<style>
.list-item {
    font-size: 16px;
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid rgb(146, 144, 144);
}
</style>

父组件:使用示例

vue 复制代码
<script setup>
import MyList from './components/MyList.vue';

const data = []
// 模拟数据
for (let i = 0; i < 100; i++) {
  data.push({
    id: i,
    name: `name${i}`,
    age: i,
    sex: i % 2 === 0 ? '男' : '女',
    address: `address${i}`,
    date: new Date(),
  })
}
</script>

<template>
  <div class="table_continue">
     <MyList :testData="data" :size="60"/>
  </div>
</template>

<style scoped>
.table_continue {
  padding: 20px;
}
</style>

四、核心实现解析

1. 计算可见区域的起始索引

javascript 复制代码
const handSrool = (e) => {
    const scrollTop = e.target.scrollTop
    startIndex.value = Math.floor(scrollTop / props.size)
}

这是虚拟列表最核心的逻辑。当用户滚动时,我们通过 scrollTop 获取滚动距离,然后除以单个列表项的高度,就能计算出当前应该从第几条数据开始渲染。

为什么要这样做?

假设每个列表项高度是 60px,滚动位置是 300px,那么 300 / 60 = 5,说明用户已经滚过了5个列表项,所以我们应该从第5条数据开始渲染。

2. 计算可见数据

javascript 复制代码
const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))

使用 slice 方法截取数组中的一部分,只渲染从 startIndex 开始的 10 条数据。这样无论原数组有多大,DOM 中最多只有 10 个列表项元素。

3. 使用 transform 模拟滚动

javascript 复制代码
const offsetY = computed(() => startIndex.value * props.size)

这是实现无缝滚动的关键。虽然我们只渲染了 10 条数据,但通过 transform: translateY() 将整个列表向下偏移,偏移量等于被"跳过"的数据的总高度。这样用户看起来就像在滚动整个列表一样。

4. 占位容器的高度

javascript 复制代码
:style="{ height:(props.size+1) * props.testData.length + 'px' }"

外层容器的高度等于单个列表项高度乘以总数据条数,这就是让滚动条看起来正常的"秘密"。浏览器会根据这个高度生成相应长度的滚动条,用户拖动滚动条时就会触发 scroll 事件。


五、性能优化建议

虽然上面的实现已经能够正常工作,但在生产环境中,你可能还需要考虑以下优化点:

1. 预渲染更多数据

可以在可见区域的前后各多渲染几条数据,避免快速滚动时出现空白:

javascript 复制代码
const BUFFER = 3
const endIndex = computed(() => startIndex.value + 10 + BUFFER)
const list = computed(() => {
    const start = Math.max(0, startIndex.value - BUFFER)
    return props.testData.slice(start, endIndex.value)
})

2. 使用防抖处理滚动事件

如果数据量非常大,可以使用防抖来减少计算频率:

javascript 复制代码
import { debounce } from 'lodash-es'

const handSrool = debounce((e) => {
    const scrollTop = e.target.scrollTop
    startIndex.value = Math.floor(scrollTop / props.size)
}, 16)

3. 处理列表项高度不一致的情况

在实际应用中,列表项的高度可能是动态的。这时可以使用测量行高的方案,或者将所有列表项设为固定高度。

4. 使用 CSS will-change 优化动画

css 复制代码
.list-item {
    will-change: transform;
}

这告诉浏览器该元素会发生变化,可以提前进行优化。


六、原理图解

让我们通过一张图来理解整个虚拟列表的工作流程:

复制代码
用户滚动 → 获取 scrollTop → 计算 startIndex → slice 取数据 → translateY 偏移
     │                                              │
     ↓                                              ↓
┌─────────┐    ┌─────────────┐    ┌──────────┐    ┌────────────┐
│ 滚动事件 │ →  │ scrollTop   │ →  │ 300/60=5  │ →  │ 取第5-15条  │
│ 触发    │    │ = 300px     │    │ start=5   │    │ translateY │
└─────────┘    └─────────────┘    └──────────┘    │ = 300px    │
                                                   └────────────┘

七、总结

虚拟列表是前端性能优化中非常重要的技术之一。它的核心思想可以用一句话概括:只渲染可见的内容,用占位符模拟完整的滚动高度

通过本文的实现,我们可以看到一个基础的虚拟列表组件并不复杂,主要涉及到以下几个关键点:

  1. 监听滚动事件:获取用户的滚动位置
  2. 计算可见索引:根据滚动位置计算应该显示的数据范围
  3. 按需渲染:只渲染计算范围内的数据
  4. 模拟滚动:通过 transform 或 padding 模拟完整的滚动高度

掌握了这些核心原理后,你就可以在此基础上进行各种定制化开发,比如支持动态高度的列表项、添加加载更多功能、实现分组虚拟列表等。

希望本文对你理解虚拟列表有所帮助!

相关推荐
早點睡3901 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-video
javascript·react native·react.js
启山智软1 小时前
【使用 Java(JSP)实现的简单商城页面前端示例】
java·前端·商城开发
Qlittleboy2 小时前
TP5.0的“请求缓存”,把页面缓存为静态HTML文件,提升加载速度
前端·缓存·html·php
請你喝杯Java2 小时前
基于 TypeScript React Next.js 的 AI 产品技术栈调研报告
javascript·react.js·typescript
Doris8932 小时前
【Node.js 】Node.js 与 Webpack 模块化工程化入门指南
前端·webpack·node.js
爱学习的程序媛2 小时前
【Web前端】“十五五”重大项目中的前端机遇
前端·科技·信息可视化·前端框架·创业创新·信息与通信
始持2 小时前
第十二讲 风格与主题统一
前端·flutter
小码哥_常2 小时前
Room 3.0大变身:安卓开发的新挑战与机遇
前端