欢迎光临
我们一直在努力

Vue中如何实现虚拟滚动处理超长列表

Vue 中如何实现虚拟滚动处理超长列表

一句话总结:上万条数据别硬渲染——虚拟滚动只渲染可视区域内的几十条,DOM 数量恒定,滚动再长的列表也不卡。

目录

  • 虚拟滚动是什么
  • 5 个高频使用场景
  • 万能兜底方案
  • 预防 Checklist
  • 一句话总结

虚拟滚动是什么

直接渲染 10000 条列表项,浏览器要创建 10000 个 DOM 节点,内存和渲染时间都爆炸。虚拟滚动(Virtual Scroll)的思路是:

  • 只渲染可视区域内的列表项(比如 20 条)
  • 用一个撑高的占位容器模拟完整列表高度
  • 监听滚动事件,动态计算当前应该渲染哪些项
  • 效果:无论列表有 1 万条还是 100 万条,DOM 数量恒定在可视区域的量级。

    方案DOM 数量首屏渲染适用场景
    直接 v-for 10000+ 卡顿数秒 小列表(< 200 条)
    分页加载 每页几十条 快,但需翻页 后台管理系统
    虚拟滚动 恒定 ~30 条 毫秒级 聊天记录、日志、大数据表格

    <!– 虚拟滚动的核心结构 –>
    <template>
    <div class="container" @scroll="onScroll" :style="{ height: '600px', overflow: 'auto' }">
    <!– 占位层:撑起总高度 –>
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
    <!– 渲染层:只包含可视区域的项,绝对定位偏移 –>
    <div :style="{ transform: `translateY(${offsetY}px)` }">
    <div v-for="item in visibleItems" :key="item.id">…</div>
    </div>
    </div>
    </div>
    </template>


    5 个高频使用场景

    场景一:直接渲染上万条数据导致页面卡死

    后端一次性返回全部数据,前端直接 v-for 渲染。

    ❌ 错误写法:全量渲染

    <template>
    <div class="list">
    <div v-for="item in list" :key="item.id" class="item">
    <span>{{ item.name }}</span>
    <span>{{ item.content }}</span>
    </div>
    </div>
    </template>

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

    const list = ref([]);

    onMounted(async () => {
    const res = await fetch('/api/logs'); // 返回 50000 条日志
    list.value = res.data;
    // 页面卡死 5-10 秒,内存占用飙升
    });
    </script>

    ✅ 正确写法:用第三方虚拟滚动库

    npm install vue-virtual-scroller

    // main.js
    import { createApp } from 'vue';
    import VirtualScroller from 'vue-virtual-scroller';
    import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

    createApp(App).use(VirtualScroller).mount('#app');

    <template>
    <RecycleScroller
    :items="list"
    :item-size="60"
    key-field="id"
    v-slot="{ item }"
    class="scroller"
    >
    <div class="item">
    <span>{{ item.name }}</span>
    <span>{{ item.content }}</span>
    </div>
    </RecycleScroller>
    </template>

    <style scoped>
    .scroller {
    height: 600px;
    }
    .item {
    height: 60px;
    display: flex;
    align-items: center;
    }
    </style>

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

    const list = ref([]);

    onMounted(async () => {
    const res = await fetch('/api/logs'); // 50000 条日志
    list.value = res.data;
    // 秒开,DOM 恒定 30 个左右
    });
    </script>

    场景二:不定高度列表的虚拟滚动

    列表项高度不一(有的单行、有的多行),固定 item-size 的方案失效。

    ❌ 错误写法:用固定高度的虚拟滚动库

    <!– item-size 只能填一个固定值,但实际每项高度不同 –>
    <RecycleScroller :items="list" :item-size="60" key-field="id">
    <!– 有的项 60px,有的项 200px,滚动定位全乱了 –>
    </RecycleScroller>

    ✅ 正确写法:用支持动态高度的 DynamicScroller

    <template>
    <DynamicScroller :items="list" :min-item-size="54" key-field="id" class="scroller">
    <template v-slot="{ item, active }">
    <DynamicScrollerItem
    :item="item"
    :active="active"
    :data-index="item.id"
    :size-dependencies="[item.content]"
    >
    <div class="item">
    <p>{{ item.name }}</p>
    <p>{{ item.content }}</p> <!– 高度随内容变化 –>
    </div>
    </DynamicScrollerItem>
    </template>
    </DynamicScroller>
    </template>

    场景三:手写虚拟滚动(不用第三方库)

    项目不想引依赖,自己实现一个基础版虚拟滚动。

    ❌ 错误写法:滚动事件里直接操作每一条数据

    const onScroll = (e) => {
    const scrollTop = e.target.scrollTop;
    // 每次滚动都重新计算并渲染整个列表
    visibleItems.value = list.value.map(/* … */);
    // 滚动 60fps 触发 60 次,每次全量计算,卡成PPT
    };

    ✅ 正确写法:计算可视窗口的起止索引

    <template>
    <div class="container" @scroll="onScroll">
    <div class="placeholder" :style="{ height: totalHeight + 'px' }">
    <div class="render-list" :style="{ transform: `translateY(${offsetY}px)` }">
    <div v-for="item in visibleItems" :key="item.id" class="item">
    {{ item.name }}
    </div>
    </div>
    </div>
    </div>
    </template>

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

    const props = defineProps({
    items: { type: Array, default: () => [] },
    itemHeight: { type: Number, default: 60 },
    containerHeight: { type: Number, default: 600 }
    });

    const scrollTop = ref(0);

    // 总高度 = 条数 × 单项高度
    const totalHeight = computed(() => props.items.length * props.itemHeight);

    // 可视区域内能容纳的条数(上下多渲染 5 条缓冲)
    const visibleCount = computed(() =>
    Math.ceil(props.containerHeight / props.itemHeight) + 10
    );

    // 起始索引(向上多渲染 5 条缓冲)
    const startIndex = computed(() =>
    Math.max(0, Math.floor(scrollTop.value / props.itemHeight) – 5)
    );

    // 可视项列表
    const visibleItems = computed(() =>
    props.items.slice(startIndex.value, startIndex.value + visibleCount.value)
    );

    // 渲染层偏移量
    const offsetY = computed(() => startIndex.value * props.itemHeight);

    const onScroll = (e) => {
    scrollTop.value = e.target.scrollTop;
    };
    </script>

    <style scoped>
    .container {
    height: 600px;
    overflow: auto;
    position: relative;
    }
    .render-list {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    }
    .item {
    height: 60px;
    display: flex;
    align-items: center;
    border-bottom: 1px solid #eee;
    }
    </style>

    场景四:滚动加载到底部自动请求下一页

    虚拟滚动 + 无限加载结合——滚动到底部时自动加载更多数据。

    ❌ 错误写法:监听 scroll 判断是否到底

    const onScroll = (e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    // 没有防抖,滚动过程中疯狂触发请求
    if (scrollTop + clientHeight >= scrollHeight) {
    loadMore(); // ❌ 一次滚动触发多次请求
    }
    };

    ✅ 正确写法:loading 锁 + RecycleScroller 事件

    <template>
    <RecycleScroller
    :items="list"
    :item-size="60"
    key-field="id"
    class="scroller"
    @reach-end="loadMore"
    v-slot="{ item }"
    >
    <div class="item">{{ item.name }}</div>
    </RecycleScroller>
    </template>

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

    const list = ref([]);
    const page = ref(1);
    const loading = ref(false);
    const hasMore = ref(true);

    const loadMore = async () => {
    if (loading.value || !hasMore.value) return; // ✅ 防重复请求锁

    loading.value = true;
    try {
    const res = await fetch(`/api/logs?page=${page.value}&size=1000`);
    const data = await res.json();
    list.value.push(…data.items);
    hasMore.value = data.hasMore;
    page.value++;
    } finally {
    loading.value = false;
    }
    };

    // 初始加载
    loadMore();
    </script>

    场景五:表格场景用专业虚拟表格库

    后台管理系统的表格有固定列、排序、筛选等复杂需求,普通虚拟列表搞不定。

    ❌ 错误写法:用 el-table 渲染大数据

    <!– Element Plus 的 el-table 全量渲染 DOM –>
    <el-table :data="hugeTableData">
    <el-table-column prop="name" label="名称" />
    <el-table-column prop="status" label="状态" />
    <el-table-column prop="createdAt" label="创建时间" />
    </el-table>
    <!– 10000 行数据直接卡死浏览器 –>

    ✅ 正确写法:用 vxe-table 虚拟滚动模式

    npm install vxe-table

    <template>
    <vxe-table
    :data="hugeTableData"
    height="600"
    :scroll-y="{ enabled: true, gt: 100 }"
    >
    <vxe-column field="name" title="名称" />
    <vxe-column field="status" title="状态" />
    <vxe-column field="createdAt" title="创建时间" />
    </vxe-table>
    </template>

    <script setup>
    // gt: 100 表示超过 100 行自动启用虚拟滚动
    // 固定列、排序、筛选都正常工作
    </script>


    万能兜底方案

    选择决策流程:

    列表数据量多大?
    ├─ < 200 条 → 直接 v-for,别过度优化
    ├─ 200 – 5000 条
    │ ├─ 后台页面 → 分页加载
    │ └─ 需要连续滚动 → 虚拟滚动
    └─ > 5000 条 → 必须虚拟滚动
    ├─ 项高度固定 → RecycleScroller / 手写
    ├─ 项高度不定 → DynamicScroller
    └─ 表格场景 → vxe-table 虚拟模式

    通用 Hook 封装:

    // useVirtualList.js
    import { ref, computed, onMounted, onBeforeUnmount } from 'vue';

    export function useVirtualList(getList, options = {}) {
    const { itemHeight = 60, bufferSize = 5 } = options;
    const containerRef = ref(null);
    const scrollTop = ref(0);
    const containerHeight = ref(600);

    const list = computed(() => getList());

    const totalHeight = computed(() => list.value.length * itemHeight);

    const visibleItems = computed(() => {
    const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) bufferSize);
    const count = Math.ceil(containerHeight.value / itemHeight) + bufferSize * 2;
    return list.value.slice(start, start + count);
    });

    const offsetY = computed(() => {
    const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) bufferSize);
    return start * itemHeight;
    });

    const onScroll = (e) => {
    // requestAnimationFrame 防抖
    requestAnimationFrame(() => {
    scrollTop.value = e.target.scrollTop;
    });
    };

    onMounted(() => {
    containerHeight.value = containerRef.value?.clientHeight || 600;
    });

    return {
    containerRef,
    totalHeight,
    visibleItems,
    offsetY,
    onScroll
    };
    }


    预防 Checklist

    检查项说明
    ☐ 列表项高度是否固定 固定用 RecycleScroller,不定用 DynamicScroller
    ☐ key 是否用了唯一 id 虚拟滚动对 key 唯一性要求更高,重复 key 会渲染错乱
    ☐ 滚动处理有没有节流 用 requestAnimationFrame 或 passive 监听,避免掉帧
    ☐ 无限加载有没有防重锁 loading 标志位防止重复请求
    ☐ 是否真的需要虚拟滚动 小于 200 条纯属过度优化
    ☐ 表格场景有没有用专业库 vxe-table / ant-design 的虚拟表格比手写更稳
    ☐ 动态高度有没有声明依赖 size-dependencies 要列出影响高度的字段

    一句话总结

    虚拟滚动的本质是"占位层撑高度 + 只渲染可视区"——固定高度用 RecycleScroller,动态高度用 DynamicScroller,表格场景用 vxe-table,超小列表别折腾。记住 DOM 数量恒定,多少条数据都不怕。


    最后问候亲爱的朋友们,并邀请你们阅读我的全新著作

    Vue.js 3企业级项目开发实战

    📚 《Vue.js 3企业级项目开发实战(微课视频版》

    赞(0)
    未经允许不得转载:171主机测评 » Vue中如何实现虚拟滚动处理超长列表
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址