vue中实现简单的虚拟长列表(等高)

1,307 阅读2分钟

虚拟长列表可应用于遇到不使用分页方式来加载长列表的需求。如在数据长度大于 1000或10000 条时,DOM 元素的创建和渲染需要的时间成本很高,完整渲染列表所需要的时间会造成很不好的用户体验,同时会存在滚动时卡顿问题

实现思路

虚拟列表的核心思想为渲染可视区域的数据列表,在页面滚动的同时对数据进行截取、复用DOM进行展示的渲染方式。

  1. 进入页面,DOM元素加载时(mounted钩子),计算出当前可视区(滚动区)的高度。同时还有计算出总数据所占的高度(滚动条的高度)
可视区的高度 = 每一项数据的高度 * 页面上显示的个数
总高度 = 每一项数据的高度 * 总列表.length
  1. 计算当前可见区域起始数据的 start 以及 当前可见区域结束数据的 end。然后在总数据列表中截取当前所需的列表数据
总数据列表.slice(从第几个数据开始, 截取到第几个数据)
  1. 监听滚动事件,通过计算出当前距离顶部的距离,然后计算出当前应该从 第几个开始显示、显示到第几个
第几个开始显示 = 当前滚动头部的距离 / 每一项的高度
显示到第几个 = 第几个开始显示 + 一页显示的个数
  1. 计算 当前显示在页面上第一个的数据 在整个列表中的偏移位置 scrollTop,并设置到列表上
可见区域起始数据的start * 每一项数据的高度

封装 ViualList.vue 组件

<template>
  <!-- 滚动列表盒子 -->
  <div class="vitual-con" ref="vitualConRef" @scroll="handleScroll">
    <!-- 滚动条 -->
    <div class="scroll-bar" ref="scrollBarRef"></div>
    <!-- 内容区域 -->
    <div class="scroll-list" ref="scrollListRef" :style="{ 'padding-top': `${scrollTop}px` }">
      <div v-for="item in visibleData" :key="item.id" class="listItem" :data-i="item.id">
        <slot name="itemList" :data="item"></slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    size: Number, // 每一项的高度
    remain: Number, // 页面显示的个数
    dataList: Array // 总列表数据
  },
  data() {
    return {
      start: 0, // 从第几个开始显示,默认从第一开始显示
      end: this.remain // 显示到第几个,默认显示到第八个(一页默认显示8个),
    }
  },
  methods: {
    handleScroll() {
      // 通过计算出当前 滚动列表盒子 距离顶部的距离,然后计算出当前应该从 第几个开始显示,显示到第几个
      //  this.start = 当前滚动头部的距离 / 每一项的高度
      //  this.end = this.start当前第一个显示项的索引 + this.remain一个显示的个数
      // ~是js里的按位取反操作符,~~就是执行两次按位取反,其实就是保持原值,这里也可以使用 Math.floor() 向下取整
      this.start = ~~(this.$refs.vitualConRef.scrollTop / this.size)
      this.end = this.remain + this.start
    }
  },
  computed: {
    // 计算 当前应该显示的数据---会根据 this.start, this.end 自动去截取要显示的数据
    visibleData() {
      let start = this.start - this.preveCount
      let end = this.end + this.nextCount
      return this.dataList.slice(start, end)
    },
    // 计算内容区域 当前距离顶部的距离----优化
    scrollTop() {
      return this.start * this.size - this.size * this.preveCount
    },
    // 优化-----避免下方出现的空白和当用户快速滚动时,出现空白屏----解决:预留加载
    preveCount() {
      // 前面预留的个数---- 当前面的个数小于 8 个时,有几个就预留几个
      return Math.min(this.start, this.remain)
    },
    nextCount() {
      // 后面预先加载的个数
      return Math.min(this.end, this.dataList.length - this.end)
    }
  },
  mounted() {
    // 1、页面一加载时,应该先计算出 滚动列表盒子 的高度 每一项的高度 * 页面显示的个数
    this.$refs.vitualConRef.style.height = this.size * this.remain + 'px'
    // 2、计算出总高度  每一项的高度 * 总列表数据的数量
    this.$refs.scrollBarRef.style.height = this.size * this.dataList.length + 'px'
  }
}
</script>

<style lang="less" scoped>
.vitual-con {
  overflow-y: scroll;
  position: relative;
}
.scroll-list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}
</style>

使用

<template>
  <div id="app">
    <!-- size 每一项的高度 -->
    <!-- remain 页面显示的个数 -->
    <!-- dataList 列表数据 -->
    <VitualList :size="40" :remain="8" :dataList="dataList">
      <template #itemList="scope">
        <div class="item">{{ scope.data.value }}</div>
      </template>
    </VitualList>
  </div>
</template>

<script>
import VitualList from '@/components/ViualList.vue'

/* 模拟数据 */
let dataList = []
for (let i = 0; i < 1000; i++) {
  dataList.push({
    id: i + 1,
    value: '当前为第' + i + '项'
  })
}
export default {
  name: 'App',
  data() {
    return {
      dataList: dataList
    }
  },
  components: {
    VitualList
  }
}
</script>

<style lang="less">
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.item {
  height: 40px;
  line-height: 40px;
  border-bottom: 1px solid #ededed;
}
</style>