Code前端首页关于Code前端联系我们

一、Vue3 虚拟滚动是啥?和普通滚动有啥不一样?

terry 1天前 阅读数 15 #Vue
文章标签 Vue3;虚拟滚动

做前端开发的朋友,肯定遇到过「列表数据太多,页面卡到动不了」的情况,Vue3 里的虚拟滚动技术,就是专门解决这种长列表性能问题的,但好多同学对虚拟滚动一头雾水:它到底是干啥的?原理咋理解?自己咋在Vue3里实现?适合啥场景?今天咱就把这些问题一个个掰碎了讲,从概念到实战,帮你把虚拟滚动彻底搞明白~

先聊聊普通滚动的问题:比如做个后台管理系统,要渲染 1 万条表格数据,普通做法是把这 1 万条数据全转成 DOM 节点,渲染到页面上,但浏览器处理几千个 DOM 就开始卡了,1 万个 DOM 直接让页面变成「PPT」——滚动卡、点击没反应,用户体验崩碎。

那虚拟滚动是咋解决的?虚拟滚动(也叫虚拟列表)的核心逻辑是「只渲染用户能看到的部分」,举个例子:列表容器高度 400px,每个列表项高 30px,那容器里最多显示 13 个项(400/30≈13),虚拟滚动只会把这 13 个(甚至多渲染几个缓冲项)真正渲染成 DOM,其他没显示的项用「占位 div」撑高度(保证滚动条长度正确),这样 DOM 节点从 1 万变成 20 个左右,性能直接起飞~

对比普通滚动就能更清楚:普通滚动是「全部渲染」,DOM 数量爆炸;虚拟滚动是「按需渲染」,只给用户看得到的内容做 DOM,性能差距天差地别,但代价是需要写代码计算「该渲染哪些项」「怎么让滚动条正常」这些逻辑。

虚拟滚动原理咋理解?这几个核心点要吃透

得把原理拆成「视觉区域、滚动容器、数据分片、占位逻辑」这几个模块,用生活化的例子讲才好懂:

  1. 视觉区域:想象你透过一扇窗户看一列火车(长列表),窗户就是「可视区域」,只能看到火车的一部分,虚拟滚动里,窗户对应的就是页面上能看到的列表容器。
  2. 滚动容器:火车轨道(滚动容器)得足够长,才能让滚动条正常,所以要用一个占位 div,高度等于「所有列表项高度之和」,让浏览器计算出正确的滚动条长度。
  3. 数据分片:火车有很多车厢(列表项),窗户每次只能显示 10 个车厢,虚拟滚动要计算「现在窗户对准第几个车厢」,然后只把这 10 个车厢(数据分片)渲染成 DOM,其他车厢先不渲染。
  4. 位移对齐:渲染出来的 10 个车厢得「贴」在窗户对应的位置,比如窗户往下滑了 300px(对应 10 个车厢高度),那渲染的车厢得用 CSS 的 transform: translateY(300px) 移动到正确位置,用户看着才像连续的列表。

举个数字例子更直观:假设列表有 1000 项,每项高 30px,容器高 600px(能显示 20 项),占位 div 高度是 1000×30 = 30000px,滚动条长度对应 30000px,当用户滚动到 scrollTop=900px 时,计算起始项是 900/30=30,所以渲染第 30 到第 50 项(20 个),然后把这 20 个项用 transform 移动 900px,这样用户看到的就是第 30 到 50 项,DOM 只有 20 个,性能拉满~

Vue3 里自己实现虚拟滚动,步骤和代码咋写?

这部分要详细讲实战,分步骤 + 代码示例 + 解释,让你能跟着做。

步骤 1:搭结构——三个核心容器

先写 template 结构,要包含「滚动容器、占位容器、可视区域容器」:

<template>
  <!-- 外层滚动容器:控制滚动,设置高度和 overflow -->
  <div class="virtual-scroll" ref="scrollRef">
    <!-- 占位容器:撑开总高度,让滚动条长度正确 -->
    <div class="placeholder" :style="{ height: totalHeight + 'px' }"></div>
    <!-- 可视区域容器:绝对定位,放实际渲染的列表项,用 transform 调整位置 -->
    <div class="visible-box" ref="visibleRef" :style="{ transform: `translateY(${offsetY}px)` }">
      <div 
        v-for="item in visibleData" 
        :key="item.id" 
        class="list-item"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

解释结构逻辑:scrollRef 是滚动容器,overflow-y: auto 开启滚动;placeholder 用高度撑开整个列表的高度,让滚动条长度正确;visible-box 绝对定位,里面放实际渲染的列表项,用 translateY 调整位置,让这些项「对齐」到滚动后的位置。

步骤 2:初始化与滚动事件绑定

用 Vue3 的 setup 语法写逻辑,核心是「计算可视项数、绑定滚动事件、处理滚动时的渲染逻辑」:

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
// 1. 模拟大量数据(10000 条)
const dataSource = ref([])
for (let i = 0; i < 10000; i++) {
  dataSource.value.push({ id: i, content: `第${i}条数据` })
}
// 2. 固定项高度(假设每个项高 30px)
const itemHeight = 30
// 3. 响应式变量:可视区域能显示的项数、起始/结束索引、位移等
const scrollRef = ref(null)       // 滚动容器 DOM
const visibleRef = ref(null)     // 可视区域 DOM
const visibleCount = ref(0)      // 可视区域能放多少项
const startIndex = ref(0)        // 渲染的起始索引
const endIndex = ref(0)          // 渲染的结束索引
const offsetY = ref(0)           // 可视区域的位移量
const visibleData = ref([])      // 实际要渲染的数据
// 总高度:所有项高度之和
const totalHeight = computed(() => dataSource.value.length * itemHeight)
// 4. mounted 时初始化
onMounted(() => {
  // 获取滚动容器的高度
  const containerHeight = scrollRef.value.clientHeight
  // 计算可视区域能放多少项(向上取整,防止有剩余高度)
  visibleCount.value = Math.ceil(containerHeight / itemHeight)
  // 绑定滚动事件
  scrollRef.value.addEventListener('scroll', handleScroll)
  // 初始计算要渲染的数据
  calculateVisibleData()
})
// 5. 卸载时移除事件(避免内存泄漏)
onUnmounted(() => {
  scrollRef.value.removeEventListener('scroll', handleScroll)
})
// 6. 滚动事件处理函数
function handleScroll() {
  // 获取滚动条滚动的距离(scrollTop)
  const scrollTop = scrollRef.value.scrollTop
  // 计算起始索引:scrollTop / 每项高度(向下取整)
  startIndex.value = Math.floor(scrollTop / itemHeight)
  // 结束索引 = 起始索引 + 可视项数
  endIndex.value = startIndex.value + visibleCount.value
  // 计算位移:起始索引 * 每项高度(让渲染的项对齐到滚动后的位置)
  offsetY.value = startIndex.value * itemHeight
  // 更新要渲染的数据
  calculateVisibleData()
}
// 7. 数据切片:只取起始到结束的部分
function calculateVisibleData() {
  visibleData.value = dataSource.value.slice(startIndex.value, endIndex.value)
}
</script>

步骤 3:样式处理(scoped CSS)

给三个容器加样式,保证布局和滚动逻辑正确:

<style scoped>
.virtual-scroll {
  height: 400px;        /* 滚动容器的高度,可根据需求改 */
  overflow-y: auto;     /* 开启垂直滚动 */
  position: relative;   /* 让内部绝对定位的元素有参考 */
  border: 1px solid #eee;
}
.placeholder {
  position: absolute;   /* 占位容器绝对定位,不影响可视区域 */
  width: 100%;          /* 宽度铺满 */
  left: 0;
  top: 0;
}
.visible-box {
  position: absolute;   /* 可视区域绝对定位,方便用 transform 移动 */
  width: 100%;
  left: 0;
  top: 0;
}
.list-item {
  height: 30px;         /* 每项高度,和 js 里的 itemHeight 一致 */
  line-height: 30px;    /* 垂直居中,优化样式 */
  border-bottom: 1px solid #eee;
}
</style>

代码解释:这里的关键是「占位容器」和「可视区域容器」的定位逻辑,占位容器用 absolute 撑开高度,让滚动条长度等于所有项的总高度;可视区域容器也是 absolute,通过 transform: translateY(offsetY) 移动,让渲染的项刚好出现在用户滚动到的位置,滚动事件里,每次计算 startIndexendIndex,然后切片数据,只渲染这部分,DOM 数量就被牢牢控制住了~

虚拟滚动适合啥场景?这些情况用了才香!

得讲清楚适用和不适用场景,避免用错地方。

适合场景

  1. 超长长列表:比如后台表格要渲染 1 万 + 条数据,或者聊天记录加载几千条历史消息,普通渲染必卡,虚拟滚动能救。
  2. 项结构简单但数量大:比如下拉选择器有几千个选项,每个选项就一行文字,虚拟滚动能大幅减少 DOM。
  3. 滚动时数据不变:如果列表项在滚动时不需要频繁更新(比如静态数据展示),虚拟滚动性能优势明显。

举个实际例子:我之前做过一个物流系统,要展示某个仓库的 10 万条库存记录,用普通渲染直接页面崩溃,换成虚拟滚动后,DOM 只有几十条,滚动丝滑得很~

不适合场景

  1. 项高度不固定:如果每个列表项高度不一样(比如有的是 30px,有的是 60px),虚拟滚动需要额外计算每个项的高度,复杂度翻倍(后面讲解决方法)。
  2. 项有复杂交互/动态内容:比如列表项里有视频自动播放、实时更新的倒计时,虚拟滚动动态渲染会导致这些内容「消失」或「重置」(因为不可视区的项会被销毁,重新渲染时状态丢失)。
  3. 数据量小:比如列表只有几百条,普通渲染性能完全够,用虚拟滚动反而多写一堆代码,没必要。

实现时容易踩的坑,咋解决?

这部分讲实际开发中的问题,帮你避坑。

坑 1:动态项高度咋处理?

问题:如果列表项高度不固定(比如内容长度不同,有的换行),之前的「固定 itemHeight」方法就失效了,滚动时会出现「内容重叠」「滚动位置错乱」。

解决方法:

  • 先测量每个项的高度:可以在页面加载时,先渲染前几个项,用 Vue 的 nextTick 获取它们的 offsetHeight,保存到一个数组里(itemHeights 数组)。
  • 动态计算总高度:总高度不再是 length * itemHeight,而是 itemHeights 数组的累加和。
  • 滚动时用二分法找起始项:因为每个项高度不同,scrollTop 对应的起始项不能简单用 scrollTop / itemHeight,得用二分法在 itemHeights 的累加数组里找位置。

举个代码思路:

// 假设 itemHeights 是保存每个项高度的数组
const itemHeights = ref([])
// 累加高度数组:[30, 60, 90, ...] 表示前 n 项的总高度
const accumulatedHeights = ref([])
// 渲染项后,测量高度(用 nextTick)
watch(visibleData, () => {
  nextTick(() => {
    const items = visibleRef.value.querySelectorAll('.list-item')
    items.forEach((item, index) => {
      const realIndex = startIndex.value + index
      itemHeights.value[realIndex] = item.offsetHeight
      // 同时更新 accumulatedHeights
      accumulatedHeights.value[realIndex] = (accumulatedHeights.value[realIndex - 1] || 0) + item.offsetHeight
    })
  })
})
// 滚动时用二分法找 startIndex
function findStartIndex(scrollTop) {
  let left = 0, right = accumulatedHeights.value.length - 1
  while (left < right) {
    const mid = Math.floor((left + right) / 2)
    if (accumulatedHeights.value[mid] <= scrollTop) {
      left = mid + 1
    } else {
      right = mid
    }
  }
  return left
}

坑 2:快速滚动时白屏

问题:如果用户快速滚动鼠标滚轮,可视区域的项还没来得及渲染,就会出现「白屏」(滚动过的地方没有内容)。

解决方法:

  • 多渲染缓冲项visibleCount 不取刚好能显示的数量,而是多取 20%(比如能显示 20 项,实际渲染 25 项),这样快速滚动时缓冲项能补上,减少白屏概率。
  • 优化滚动事件:用节流(throttle)减少 handleScroll 的执行频率,但要注意节流时间不能太长,否则滚动不流畅,一般用 16ms(约 60 帧)的节流间隔。

坑 3:滚动位置计算错误

问题:滚动容器有 paddingborder,导致 scrollTop 获取不准确,渲染的项位置偏移。

解决方法:

  • 准确计算「可滚动区域」:用 scrollRef.value.scrollTop的顶部距离容器顶部的距离),而容器的 padding 要算在容器高度里,比如容器高度是 clientHeight(包含 padding),所以计算 visibleCount 时用 clientHeight 是对的。
  • 调试时用 console.log 打印 scrollTopstartIndexoffsetY 等值,看是否和预期一致。

不想自己写?Vue 生态有现成的虚拟滚动组件!

讲社区库,降低开发成本。

推荐库:vue-virtual-scroller(Star 数多,维护活跃)

用法示例:

  1. 安装:npm install vue-virtual-scroller

  2. 全局注册(main.js):

    import { createApp } from 'vue'
    import App from './App.vue'
    import { VirtualScroller } from 'vue-virtual-scroller'
    import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const app = createApp(App) app.component('VirtualScroller', VirtualScroller) app.mount('#app')


3. 组件中使用:  
```html
<template>
  <VirtualScroller
    class="my-scroller"
    :items="bigData"
    :item-size="30"
    key-field="id"
  >
    <template #default="{ item }">
      <div class="item">{{ item.content }}</div>
    </template>
  </VirtualScroller>
</template>
<script setup>
import { ref } from 'vue'
const bigData = ref(/* 10000 条数据 */)
</script>
<style scoped>
.my-scroller {
  height: 400px;
}
.item {
  height: 30px;
  line-height: 30px;
}
</style>

库的优势:vue-virtual-scroller 帮我们封装了滚动计算、动态高度处理、性能优化这些复杂逻辑,比如要支持动态高度,只需要把 item-size 改成一个函数,返回每个项的高度;要做网格布局(比如瀑布流),也有对应的 GridScroller 组件,适合不想自己折腾底层逻辑,想快速实现虚拟滚动的同学。

但也要提醒:用现成库省事儿,但遇到特别定制化的需求(比如列表项有复杂动画、特殊滚动逻辑),还是得理解虚拟滚动原理,才能改库或者自己写逻辑~

虚拟滚动啥时候用?咋用?

最后总结,帮你理清思路:

  • 用的时机:数据量 ≥ 1 万,普通渲染卡;列表项结构简单,交互少;需要优化性能。
  • 实现方式:简单场景(固定高度

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门