Vue3中利用Javascript实现虚拟列表 | 豆包MarsCode AI刷题

54 阅读4分钟

众所周知,我是非常喜欢听歌的一个人,但是苦于喜欢的歌有一些是翻唱,网易云上没有,所以自己用Electron做了个听歌的软件,支持歌单混合网易云等多个来源的歌曲,但是有个问题,一个歌单里的歌曲可能会很多,甚至可能上千,在我初期没管这个问题,但是前一段时间注意到了,并且之前听说过虚拟列表这个东西,据说是不管多少数据,只需要加载几个十几个元素,避免了不可见的元素也大量渲染。本来应该是介绍原生Javascript实现的,但是这篇文章是以现有项目为基础,所以直接以我自己的项目为例,介绍一下我的虚拟列表实现,我最后会提到如何转成原生JS的。

效果展示

事先说明,这纯个人使用,也是通过我自己的VIP获取的歌曲数据 image.png

页面中下部分的列表,就是我实现虚拟列表之后的,可从"Total 312"看出,这歌单总共有312首歌,但是我只需要渲染9个列表元素,以及一个wrapper元素

image.png

原理

总体原理

就是利用一个空内容的wrapper元素撑起元素的高度,同时显示滚动条,然后利用一个绝对定位的list元素显示应该出现在视野内的元素,并且动态更新就好了(这里的动态更新是vue提供的能力),通过计算

几个注意

  1. 这里实现的是固定高度的列表元素,因为需要计算渲染几个元素,所以需要提前设置每个元素的高度

wrapper

这个元素是空的,但是高度为总元素个数 × 每个元素的高度
<div class="wrapper" :style="{height: '${itemHeight * items.length}'px}"></div>

list

<div class="list" :style="{transform: `translateY(${-transform + scrollTop}px)`}">
  <div class="list-item"
       v-for="(item, index) in displayingItems"
       :key="startIndex + index"
       :style="{ height: itemHeight + 'px' }">
    <slot :item="item" :index="startIndex + index"></slot>
  </div>
</div>

这里的list使用绝对定位,定位在top, bottom, left, right都是0

对于不会Vue3的人,v-for就是根据给的数组生成多个相同标签元素的的东西,自己写就是多写几个list-item元素就好了,key不用管, style就是style,slot不用管,就当成实际用的时候的具体的列表元素的内容就行了(innerHTML)

数据计算

const scrollTop = ref(0);

const displayingItems = computed(() => {
  return items.slice(startIndex.value, endIndex.value)
})
const startIndex = computed(() => {
  return Math.floor(scrollTop.value / itemHeight)
})
const endIndex = computed(() => {
  return startIndex.value + size + 1;
});
const transform = computed(() => {
  return scrollTop.value % itemHeight
})

scrollTop: 是那个容器的scrollTop

ref(0)可以直接当成0,但是用到这个变量的地方都会在这个变量更新的时候更新,computed同理

displayingItems: 需要渲染的元素 startIndex, endIndex, 需要渲染的元素在原数组中的开始索引和结束索引 tranform: 自动计算的list元素要偏移多少,因为在你滚动容器的时候,绝对定位的列表也需要偏移,才能正确显示在该显示的位置

完整实现

<script setup lang="ts">
import {computed, watch, ref, useTemplateRef, onUnmounted, nextTick} from "vue";
const {
  items,
  itemHeight,
  className = "",
  size = 10,
  eventName = "virtualList-refresh"
} = defineProps<{
  items: Array<any>,
  itemHeight: number,
  className?: string,
  size?: number,
  eventName ?: string,
}>()
const scrollTop = ref(0);

const displayingItems = computed(() => {
  return items.slice(startIndex.value, endIndex.value)
})
const startIndex = computed(() => {
  return Math.floor(scrollTop.value / itemHeight)
})
const endIndex = computed(() => {
  return startIndex.value + size + 1;
});
const transform = computed(() => {
  return scrollTop.value % itemHeight
})
const containerEl = useTemplateRef<HTMLDivElement>('container')
// console.log(displayingItems.value)
function handleScroll() {
  if (!containerEl.value) return;
  scrollTop.value = 0;
  nextTick(() => scrollTop.value = containerEl.value!.scrollTop)
  // console.log(scrollTop.value)
}
function refresh() {
  handleScroll()
}
watch(() => items, refresh)
</script>

<template>
  <div ref="container" :class="className" class="list-container" @scroll.passive="handleScroll">
    <div class="list" :style="{transform: `translateY(${-transform + scrollTop}px)`}">
      <div class="list-item"
           v-for="(item, index) in displayingItems"
           :key="startIndex + index"
           :style="{ height: itemHeight + 'px' }">
        <slot :item="item" :index="startIndex + index"></slot>
      </div>
    </div>
    <div class="wrapper" :style="{height: `${itemHeight * items.length}px`}"></div>
  </div>
</template>

<style scoped>
.list-container {
  position: relative;
  height: 100%;
  width: 100%;
  overflow-y: auto;
}
.list {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}
</style>

原生JS实现方法

  1. 把ref, computed等去掉,并且自己实现响应式(可以通过defineProperty实现自定义setter时更新),也可以写个更新函数,在滚动事件触发时更新需要更新的数据。
  2. 把slot换成实际的内容,因为slot是个抽象的概念,用原生js需要换成实际的列表元素内容
  3. v-for改成多个元素,可以写死,也可以写个函数,判断数量和之前不同就通过js渲染
  4. 数据计算部分同1部分,写成手动实现的更新数据

总结

优点

  1. 在大数据量时候确实改善了dom渲染的负担,加快了滚动速度

缺点

  1. 实现有点麻烦
  2. 实际使用中,滚动过快可能出现刷新不及时出现空白部分(不是重度强迫症基本没什么影响)

个人感受

我本来写虚拟列表是为了优化性能,但实际上用处不是太大,主要还是拓展了自己的技术,因为都是百万级或以上的数据才会用到虚拟列表,如果是自己的项目还好说,加就加了,但是如果是现有的项目,最好还是不要主动重构列表显示部分,要不然可能吃力不讨好。总体而言,虚拟列表在处理长列表时具有重要价值,但也需要在实际应用中权衡其优缺点。