虚拟列表(Virtual List)组件实现与优化铁臂猿版(简易版)

引入:本文使用vue3+ts+tailwindcss实现

一、为什么需要"虚拟列表"?

当我们在页面中渲染一个很长的列表 时(比如上万条数据),普通的v-for会一次性创建上万个DOM节点:

vue 复制代码
<li v-for="item in 10000">{{ item }}</li>

这会导致:

  • 页面加载慢;
  • 内存占用大;
  • 滚动卡顿。

于是就有了性能神器------虚拟列表 (Virtual List)

二、核心思想

虚拟列表的核心实现思路其实很简单:

固定外层盒子的高,通过计算只渲染出可见区域的数据,其余不可见元素用一个占位元素撑出滚动条。

可视化理解

假设列表共有 10000 条,每行高 40px:

区域 说明
可见区域 一次只显示约 10 行
缓冲区 上下各多渲染几行防止闪烁
占位容器 用总高度撑出滚动条
渲染窗口 动态平移(translateY)到对应位置

这样:

  • 页面上实际只有几十个 DOM
  • 但滚动条看起来依然完整;
  • 用户看起来就像全部都渲染出来一样。

三、基础版虚拟列表示例

我们先用最简单的方法来实现一个简易版的,以便我们更好的理解最底层的思想原理:

  • 支持滚动
  • 支持滚动数据切片
  • 支持缓冲区

JS部分实现思路:

用一个固定高度的容器制造滚动条,通过计算只渲染可视区域的数据,并用偏移量让这些数据看起来在正确的位置。

1. 主要变量设置,本教程为简易版,默认所有数据项等高

js 复制代码
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
/**
 * 组件参数(Props)
 */
const props = defineProps({
  items: { type: Array, default: () => [] }, // 数据数组
  itemHeight: { type: Number, default: 40 }, // 单行高度
  height: { type: Number, default: 300 },     // 容器高度
  buffer: { type: Number, default: 5 }        // 上下缓冲行数
})
/**
 *  状态变量(会随滚动变化)
 */
const containerRef = ref<HTMLDivElement>()   // 滚动容器 DOM
const scrollTop = ref(0)                     // 当前滚动位置
</script>

2.可视区域计算:包括高度计算和偏移量(offset)计算

js 复制代码
// 总高度(撑出滚动条):总数居条数*每一项高度
const totalHeight = computed(() => props.items.length * props.itemHeight)

// 当前一屏可显示多少行(加上缓冲):可视区高度/每一项高度
const visibleCount = computed(() => {
  const base = Math.ceil(props.height / props.itemHeight)
  return base + props.buffer * 2
})

// 可见数据的起止索引:
// 1. 当前位置 ÷ 每一项高度 = 当前完全滚过多少项(向下取整得到最完整的已滚过项数)
// 2. 减去缓冲区:因为上方缓冲区的内容也需要提前渲染
// 3. 确保索引不小于0(边界保护)
const startIndex = computed(() => {
  const start = Math.floor(scrollTop.value / props.itemHeight) - props.buffer
  return start < 0 ? 0 : start
})

// 结束索引 = 开始索引 + 要渲染的总数量(可见区+上下缓冲区)
// 注意:这里应该加上边界检查,防止超过数组长度
const endIndex = computed(() => {
  const end = startIndex.value + visibleCount.value
  return end > props.items.length ? props.items.length : end
})
// 当前可见的数据切片
const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value))

// 平移偏移量(用于 translateY)
const offsetTop = computed(() => startIndex.value * props.itemHeight)

3.滚动事件监听

js 复制代码
const onScroll = () => {
//这里加一个判断防止DOM为渲染发生闪烁
  if (!containerRef.value) return
  scrollTop.value = containerRef.value.scrollTop
}

onMounted(() => {
  // 初始化计算,避免第一次渲染闪烁
  onScroll()
})

HTML部分实现思路及代码:

包括三层的盒子:

第一层 :最外层固定高度的盒子,负责滚动 第二层 :撑开高度,让滚动条看起来像有全部数据 第三层:可视区域,只渲染看得见的几条数据,跟着滚动动态移动

js 复制代码
<template>
  <!-- 外层容器:固定高度 + 滚动 -->
  <div
    ref="containerRef"
    class="overflow-auto border rounded"
    :style="{ height: `${props.height}px` }"
    @scroll="onScroll"
  >
    <!-- 占位容器:撑起滚动条 -->
    <div :style="{ height: `${totalHeight}px`, position: 'relative' }">
      <!-- 渲染窗口:通过 translateY 偏移 -->
      <div :style="{ transform: `translateY(${offsetTop}px)` }">
        <div
          v-for="(item, idx) in visibleItems"
          :key="startIndex + idx"
          class="border-b px-3 flex items-center"
          :style="{ height: `${props.itemHeight}px` }"
        >
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>

父组件使用实例:

js 复制代码
<template>
  <VirtualList :items="list" :height="400" :itemHeight="35" />
</template>

<script setup lang="ts">
import VirtualList from './VirtualList.vue'

// 模拟 1 万条数据
const list = Array.from({ length: 10000 }, (_, i) => `第 ${i + 1} 条数据`)
</script>

四、优化方案:让虚拟列表更丝滑

上面的基础版我们已经实现了最底层必要的代码,但在真实项目中可能还有下列问题:

  • 滚动事件触发太频繁
  • 容器尺寸改变没有自动适配
  • 想要暴露API比如:"滚动到第N行"
  • item如果不是字符串而是对象,访问属性会类型报错

接下来我们给这个简易版的虚拟列表加点小优化

1.使用requestAnimationFrame节流滚动事件

原因: 滚动事件(scroll)触发频率极高(每秒上百次),频繁计算会导致页面卡顿。

解决: 利用浏览器的"下一帧更新"机制

js 复制代码
let rafId: number | null = null
const onScroll = () => {
  if (!containerRef.value) return
  const top = containerRef.value.scrollTop

  // 如果上一帧任务还没执行完,就取消它
  if (rafId !== null) cancelAnimationFrame(rafId)

  // 只保留最后一次
  rafId = requestAnimationFrame(() => {
    scrollTop.value = top
    rafId = null
  })
}

原理

  • requestAnimationFrame让代码在"下一次屏幕绘制"前执行;
  • 每帧约16ms,即使滚动再快,最多60次/秒,性能稳定

2.支持"滚动到指定行"

想让列表自动滚动第200行怎么做?

js 复制代码
function scrollToIndex(index: number) {
  if (!containerRef.value) return
  containerRef.value.scrollTop = index * props.itemHeight
}
defineExpose({ scrollToIndex })

父组件调用:

vue 复制代码
<button @click="goTo200">滚动到第 200 行</button>
<VirtualList ref="listRef" :items="list" />
ts 复制代码
const listRef = ref()
const goTo200 = () => listRef.value?.scrollToIndex(200)

3.类型安全展示(防止TS报错)

如果数据是对象,直接写item.id会报错(因为item是unknown) 解决:封装一个安全函数。

js 复制代码
function displayText(item: unknown): string {
  if (item && typeof item === 'object') {
    const r = item as Record<string, unknown>
    return String(r.label ?? r.id ?? '[Object]')
  }
  return String(item)
}
//模板
{{ displayText(item) }}

4.自动适配容器高度变化

容器可能因父元素布局变化而高度改变,使用ResizeObserver自动监听。

ts 复制代码
import { onBeforeUnmount } from 'vue'

let resizeObserver: ResizeObserver | null = null
onMounted(() => {
  const el = containerRef.value
  if (el) {
    resizeObserver = new ResizeObserver(() => {
      // 重新计算高度
      containerHeight.value = el.clientHeight
    })
    resizeObserver.observe(el)
  }
})
onBeforeUnmount(() => resizeObserver?.disconnect())

五、最终结果

现在实现的虚拟列表:

  • 流畅不卡顿;
  • 类型安全;
  • 可主动滚动;
  • 自适应容器变化;
  • 对象数据、字符串数据都能显示;
  • 真正做到 性能与体验两不误

六、总结:

模块 作用
totalHeight 撑出滚动条
visibleItems 控制渲染数量
offsetTop 平移到正确位置
requestAnimationFrame 优化高频滚动性能
scrollToIndex 提供外部控制
ResizeObserver 自适应容器尺寸
相关推荐
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶1 天前
前端交互规范(Web 端)
前端
像我这样帅的人丶你还1 天前
别再让JS耽误你进步了。
css·vue.js
@yanyu6661 天前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能
王霸天1 天前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航1 天前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界1 天前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript