Skip to content
目录

虚拟列表-渲染 10 万条数据

简介

虚拟列表是一种优化长列表渲染的技术,它可以在保持流畅性的同时,渲染大量的数据。

在传统的列表渲染中,如果列表非常长,会导致渲染时间过长,页面卡顿,用户体验变得非常差。而虚拟列表则是只渲染可见区域内的数据,而非全部渲染,这样就可以大大提高渲染效率,保持页面流畅性。

应用场景

虚拟列表技术在大数据量列表渲染场景中应用广泛,例如电商商品列表、社交动态列表等。

我们主要围绕以下几个部分讲解:

  • 虚拟列表实现原理
  • VueUse 方案
  • vue-virtual-scroller 方案

演示环境

  • 电脑 - Windows 10
  • Node - v16.15.0
  • Npm - 9.4.0
  • Express - 4.18.2

后端服务

为了便于演示,我们用 express 返回 10 万条数据。

js
const express = require('express')
const cors = require('cors')
// 创建 express 实例
const app = express()

// 允许跨域
app.use(cors())
// 主机
const host = 'http://localhost'
// 端口
const port = 3000

// 获取十万条数据
app.get('/large-data', (req, res) => {
  // 定义数组,用于存放十万条数据
  const data = []
  // 循环十万次
  for (let i = 1; i <= 100000; i++) {
    // 循环添加十万条数据
    data.push({ id: i, name: `名字${i}`, value: i })
  }
  // 返回十万条数据
  res.send({ code: 0, msg: '成功', data })
})

// 启动服务器
app.listen(port, () => {
  console.log(`${host}:${port}`)
})

虚拟列表-实现原理

vue
<script setup lang="ts">
import axios from 'axios'
import { computed, onMounted, ref } from 'vue'

type Item = {
  id: number
  name: string
}

// 所有的数据,比如这个数组存放了十万条数据
const allListData = ref<Item[]>([])

const itemHeight = ref(40) // 每一条(项)的高度,比如 40 像素
const count = ref(10) // 一屏展示几条数据
const startIndex = ref(0) // 开始位置的索引
const endIndex = ref(10) // 结束位置的索引
const topVal = ref(0) // 父元素滚动位置

// 计算展示的列表
const showListData = computed(() => allListData.value.slice(startIndex.value, endIndex.value))

// 获取十万条数据
const getData = async () => {
  const res = await axios.get('http://localhost:3000/large-data')
  allListData.value = res.data.data
}

// 初始化加载
onMounted(() => {
  getData()
})

// 虚拟列表视口
const viewport = ref<HTMLDivElement>()

// 滚动这里可以加上节流,减少触发频次
const handleScroll = () => {
  // 非空判断
  if (!viewport.value) return
  // 获取滚动距离
  const scrollTop = viewport.value.scrollTop
  // 计算起始下标和结束下标,用于 computed 计算
  startIndex.value = Math.floor(scrollTop / itemHeight.value)
  endIndex.value = startIndex.value + count.value
  // 动态更改定位的 top 值,确保联动,动态展示相应内容
  topVal.value = viewport.value.scrollTop
}
</script>

<template>
  <h2>手写虚拟列表-原理{{ topVal }}</h2>
  <!-- 
    虚拟列表容器:类似“视口”,视口的高度取决于一次展示几条数据
    比如视口只能看到10条数据,一条40像素,10条400像素
    故,视口的高度为400像素,注意要开定位和滚动条 
  -->
  <div
    class="viewport"
    ref="viewport"
    @scroll="handleScroll"
    :style="{ height: itemHeight * count + 'px' }"
  >
    <!-- 占位 dom 元素,其高度为所有的数据的总高度 -->
    <div class="placeholder" :style="{ height: allListData.length * itemHeight + 'px' }"></div>
    <!-- 内容区,展示10条数据,注意其定位的top值是变化的 -->
    <div class="list" :style="{ top: topVal + 'px' }">
      <!-- 每一条(项)数据 -->
      <div
        v-for="item in showListData"
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
// 虚拟列表容器盒子
.viewport {
  box-sizing: border-box;
  width: 240px;
  border: solid 1px #000000;
  // 开启滚动条
  overflow-y: auto;
  // 开启相对定位
  position: relative;
  .list {
    width: 100%;
    height: auto;
    // 搭配使用绝对定位
    position: absolute;
    top: 0;
    left: 0;
    .item {
      box-sizing: border-box;
      width: 100%;
      height: 40px;
      line-height: 40px;
      text-align: center;
      // 隔行变色
      &:nth-child(even) {
        background: #c7edcc;
      }
      &:nth-child(odd) {
        background: pink;
      }
    }
  }
}
</style>

VueUse 组合式函数

安装依赖

sh
pnpm install @vueuse/core

参考链接:https://vueuse.org/core/useVirtualList/

vue
<script setup lang="ts">
import { useVirtualList } from '@vueuse/core'
import axios from 'axios'
import { onMounted, ref } from 'vue'

type Item = {
  id: number
  name: string
}

// 所有的数据,比如这个数组存放了十万条数据
const allListData = ref<Item[]>([])

// 获取十万条数据
const getData = async () => {
  const res = await axios.get('http://localhost:3000/large-data')
  allListData.value = res.data.data
}

// 初始化加载
onMounted(() => {
  getData()
})

// 每一项的高度,比如 40 像素
const itemHeight = ref(40)

// vueuse方案:https://vueuse.org/core/useVirtualList/
const { list, containerProps, wrapperProps } = useVirtualList(allListData, {
  itemHeight: itemHeight.value,
})
</script>

<template>
  <h2>虚拟列表-VueUse实现</h2>
  <div v-bind="containerProps" class="viewport">
    <div v-bind="wrapperProps" class="list">
      <div v-for="item in list" :key="item.data.id" class="item">
        {{ item.data.name }}
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
// 虚拟列表容器盒子
.viewport {
  box-sizing: border-box;
  width: 240px;
  height: 400px;
  border: solid 1px #000000;
  // 开启滚动条
  overflow-y: auto;
  // 开启相对定位
  position: relative;
  .list {
    width: 100%;
    height: auto;
    // 搭配使用绝对定位
    position: absolute;
    top: 0;
    left: 0;
    .item {
      box-sizing: border-box;
      width: 100%;
      height: 40px;
      display: flex;
      justify-content: center;
      align-items: center;
      // 隔行变色
      &:nth-child(even) {
        background: #c7edcc;
      }
      &:nth-child(odd) {
        background: pink;
      }
    }
  }
}
</style>

vue-virtual-scroller 组件

安装依赖

sh
pnpm install vue-virtual-scroller@next

进阶用法:https://github.com/Akryum/vue-virtual-scroller

vue
<script setup lang="ts">
import axios from 'axios'
import { onMounted, ref } from 'vue'

// https://github.com/Akryum/vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

type Item = {
  id: number
  name: string
}

// 所有的数据,比如这个数组存放了十万条数据
const allListData = ref<Item[]>([])

// 获取十万条数据
const getData = async () => {
  const res = await axios.get('http://localhost:3000/large-data')
  allListData.value = res.data.data
}

// 初始化加载
onMounted(() => {
  getData()
})

// 每一项的高度,比如 40 像素
const itemHeight = ref(40)
</script>

<template>
  <h2>虚拟列表-vue-virtual-scroller实现</h2>
  <RecycleScroller
    class="viewport"
    :items="allListData"
    :item-size="itemHeight"
    key-field="id"
    v-slot="{ item }"
    item-class="item"
  >
    {{ item.name }}
  </RecycleScroller>
</template>

<style scoped lang="scss">
// 虚拟列表容器盒子
.viewport {
  box-sizing: border-box;
  width: 240px;
  height: 400px;
  border: solid 1px #000000;
  // 开启滚动条
  overflow-y: auto;
  // 开启相对定位
  position: relative;

  :deep(.item) {
    box-sizing: border-box;
    width: 100%;
    height: 40px;
    display: flex;
    justify-content: center;
    align-items: center;
    // 隔行变色
    &:nth-child(even) {
      background: #c7edcc;
    }
    &:nth-child(odd) {
      background: pink;
    }
  }
}
</style>

总结

虚拟列表是一种优化大数据量列表渲染性能的技术。其本质是只渲染当前可见区域内的列表项,而不是全部渲染。当用户滚动列表时,虚拟列表会动态计算当前可见区域内需要渲染的列表项,并只渲染这些列表项,从而提高渲染效率。

虚拟列表的实现原理一般是通过计算容器的高度和每个列表项的高度,来确定当前可见区域内需要渲染的列表项数量和位置。

  • 虚拟列表实现原理
  • VueUse 实现简单需求
  • vue-virtual-scroller 组件实现复杂需求

应用场景

虚拟列表技术在大数据量列表渲染场景中应用广泛,例如电商商品列表、社交动态列表等。

拓展阅读

根据 MIT 许可证发布