虚拟列表的简单实现以及现有库的使用

86 阅读3分钟

使用场景

业务中不可避免会遇到数据量非常非常多,下拉列表渲染的话要拉很久而且可能会非常卡.这对用户体验来说是非常不友好的所以要使用一些方式来进行优化.常用的有懒加载,分页,带搜索的下拉(请求的分页数据不要太多),还有就是虚拟列表(主要针对于不好分页的情况).

基本原理

1:获取数据,定义item盒子的css高度获取可视区的高度;总体有三个大盒子,一个可视区盒子,一个撑开高度的盒子和一个渲染数据的盒子.

  <!-- 可视区盒子 -->
  <div
    class="infinite-list-container"
    ref="infiniteListContainer"
    @scroll="(e) => onScroll(e)"
    :style="{ height: `${screenHeight}px` }"
  >
  <!-- 撑开高度的盒子 -->
    <div
      class="infinite-list-phantom"
      :style="{ height: `${listHeight}px` }"
    ></div>
    <!-- 渲染数据的盒子 -->
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        class="infinite-item"
        v-for="item in visibleData"
        :key="item"
        :style="{
          height: `${itemSize}px`,
          lineHeight: `${itemSize}px`,
        }"
      >
        {{ item }}
      </div>
    </div>
  </div>

2:通过计算列表数据的长度乘以单个元素的高度展示项的高度来得出列表的总高度(站在可视区背后的盒子,用来撑开可视区获得滚动条)

  //列表总高度
  const listHeight = ref(0);
  //列表总高度
  listHeight.value = data.length * itemSize.value;

3:初始化可视区数据的开始下标和结束下标用于截取数据;然后用可视区的高度/子项的高度获得可视区需要展示的数据数量,并用这个数量来获取结束索引的下标;

    // 可视区域数据起始索引
    const startIndex = ref(0);
    // 可视区域结束索引
    const endIndex = ref("");
    // 当前显示的数据列表
    const visibleData = ref([]);
    //当前显示的列表数字
    const visibleCount = ref(0);
    
    // 获取初始数据,向上取整首屏展示列表数量
    visibleCount.value = Math.ceil(screenHeight.value / itemSize.value);
    // 获取结束位置索引
    endIndex.value = startIndex.value + visibleCount.value;
    // 获取当前列表需要展示的数据
    visibleData.value = data.slice(startIndex.value, endIndex.value);

4:获取滚动的距离和滚动的偏移量来得出接下来需要截取的数据,也就是开始索引结束索引.然后用这个索引截取数据.并且根据偏移量用css定位把滚动条滚动的距离还原(数据替换了但是被滚上去了)

const onScroll = ({ target }) => {
  /**
   *  列表总高度listHeight = data.length * itemSize
      可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
      数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
      数据的结束索引endIndex = startIndex + visibleCount 
      列表显示数据为visibleData = data.slice(startIndex,endIndex)
      偏移量startOffset = scrollTop - (scrollTop % itemSize);
   */
  // 这里的偏移量说白了就是防止滚动造成的动画效果消失,如果没有偏移量那么滚动的距离跟移动的一样相等于可视列表是不动的.只有数据在变化
  // 所以我们要在列表没滚动到一个itemSize的时候不去干涉他获得浏览器的滚动效果,而在滚动到一个itemSize或者其倍数的时候的时候再去移动把滚动到上面的数据滚动下来这样就会有滚动的效果又不影响展示(第一个数据被滚动上去了)
      scrollTop.value = target.scrollTop;
      startIndex.value = Math.floor(scrollTop.value / itemSize.value);
      endIndex.value =
        startIndex.value + Math.ceil(screenHeight.value / itemSize.value);
      startOffset.value = target.scrollTop;
      console.log(target.scrollTop, startOffset.value);
      // startOffset.value = startIndex.value * itemSize.value;
      visibleData.value = data.slice(startIndex.value, endIndex.value + 1);
};

以下是完整代码

<template>
  <!-- 虚拟列表 -->
  <h1>简单实现</h1>
  <!-- 可视区盒子 -->
  <div
    class="infinite-list-container"
    ref="infiniteListContainer"
    @scroll="(e) => onScroll(e)"
    :style="{ height: `${screenHeight}px` }"
  >
  <!-- 撑开高度的盒子 -->
    <div
      class="infinite-list-phantom"
      :style="{ height: `${listHeight}px` }"
    ></div>
    <!-- 渲染数据的盒子 -->
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        class="infinite-item"
        v-for="item in visibleData"
        :key="item"
        :style="{
          height: `${itemSize}px`,
          lineHeight: `${itemSize}px`,
        }"
      >
        {{ item }}
      </div>
    </div>
  </div>
</template>

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

// 获取盒子的dom
const infiniteListContainer = ref(null);
const data = new Array(100000).fill(0).map((_, i) => i);
// 可视区域数据起始索引
const startIndex = ref(0);
// 可视区域结束索引
const endIndex = ref("");
// 可视区域的高度(固定)
const screenHeight = ref(600);
// 列表高度(计算)
const itemSize = ref(100);
// 当前滚动位置
const scrollTop = ref(0);
// 滚动位置偏移量
const startOffset = ref(0);
// 当前显示的数据列表
const visibleData = ref([]);
//当前显示的列表数字
const visibleCount = ref(0);
//列表总高度
const listHeight = ref(0);

const getTransform = computed(() => {
  return `translateY(${startOffset.value}px)`;
});
onMounted(() => {
  // 获取初始数据,向上取整首屏展示列表数量
  visibleCount.value = Math.ceil(screenHeight.value / itemSize.value);
  // 获取结束位置索引
  endIndex.value = startIndex.value + visibleCount.value;
  // 获取当前列表需要展示的数据
  visibleData.value = data.slice(startIndex.value, endIndex.value);
  //列表总高度
  listHeight.value = data.length * itemSize.value;
});

const onScroll = ({ target }) => {
  /**
   *  列表总高度listHeight = data.length * itemSize
      可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
      数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
      数据的结束索引endIndex = startIndex + visibleCount 
      列表显示数据为visibleData = data.slice(startIndex,endIndex)
      偏移量startOffset = scrollTop - (scrollTop % itemSize);
   */
  // 这里的偏移量说白了就是防止滚动造成的动画效果消失,如果没有偏移量那么滚动的距离跟移动的一样相等于可视列表是不动的.只有数据在变化
  // 所以我们要在列表没滚动到一个itemSize的时候不去干涉他获得浏览器的滚动效果,而在滚动到一个itemSize或者其倍数的时候的时候再去移动把滚动到上面的数据滚动下来这样就会有滚动的效果又不影响展示(第一个数据被滚动上去了)
  scrollTop.value = target.scrollTop;
  startIndex.value = Math.floor(scrollTop.value / itemSize.value);
  endIndex.value =
    startIndex.value + Math.ceil(screenHeight.value / itemSize.value);
  startOffset.value = target.scrollTop;
  console.log(target.scrollTop, startOffset.value);
  // startOffset.value = startIndex.value * itemSize.value;
  visibleData.value = data.slice(startIndex.value, endIndex.value + 1);
};
</script>

<style scoped>
.infinite-list-container {
  width: 300px;
  height: 600px;
  overflow: auto;
  position: relative;
  border: 1px solid #ccc;
  .infinite-list-phantom {
    position: absolute;
    width: 100%;
    background-color: #00b578;
    z-index: -1;
  }
  .infinite-list {
    position: absolute;
    width: 100%;
    text-align: center;
    .infinite-item {
      box-sizing: border-box;
      border: 1px solid #b3b5b9;
    }
  }
}
</style>

vue virtual scroller 实现虚拟列表

实际工作中使用自己实现的虚拟列表还是有点风险,所以我们可以使用成熟的组件来实现.vue virtual scroller就可以满足我们的要求

安装和使用vue virtual scroller (vue3)

npm install --save vue-virtual-scroller@next

//main.js
import "vue-virtual-scroller/dist/vue-virtual-scroller.css" 
import VueVirtualScroller from "vue-virtual-scroller" 
import "virtual:uno.css";
app.use(VueVirtualScroller);

这里只展示一下RecycleScroller组件的使用,需要去指定渲染盒子的高度

<template>
  <h2>使用vue virtual scroller 实现虚拟列表</h2>
  <RecycleScroller
    class="scroller"
    :items="list"
    :item-size="32"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="user">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import {ref} from 'vue'

/* 
     key-field用来指定item内的对象唯一标识
     item-size用来指定item的高度,一般通过css设置
     RecycleScroller的高度也用css指定,一般是可视区域的高度
*/
const list = ref(Array.from({ length: 100000 }, (_, index) => ({
  id: index,
  name: `User ${index}`
})))

</script>

<style scoped>
.user {
  height: 32px;
  line-height: 32px;
  padding: 0 12px;
  border-bottom: 1px solid #f0f0f0;
}
.scroller {
  width: 300px;
  height: 200px;
}
</style>