虚拟列表

446 阅读2分钟

虚拟列表

虚拟列表是指只在用户可视区域内的列表数据进行渲染,在不可见区域的数据不进行渲染或者进行部分渲染。

为什么要使用虚拟列表

工作中经常会遇到滚动分页的业务场景,不同于普通分页。普通分页在分页查询后只会渲染展示当前页的数据。而滚动分页则会在每次查询之后,将数据插入到之前数据的尾部,将全部的数据都进行渲染展示。当用户滚动多次之后,数据量很大,将这些数据全部渲染出来会占用很多内存,于是就会出现滚动卡顿的情况。而使用虚拟列表时,只会渲染可视区域内的列表数据,所以会提高渲染性能,优化用户体验。

虚拟列表的原理

  1. 获取可视区域高度,列表项高度,计算可展示的数量。
  2. 通过展示数量获取展示项结束索引以及实际展示列表。
  3. 当滚动时动态计算开始索引,结束索引以及实际展示列表,对可视区域重新进行渲染。

注意点:

  • 由于只对可视区域内的列表进行渲染,所以不会出现滚动条,就需要一个与实际列表总高度一致的div进行占位,还原滚动条。
  • 滚动时会将实际展示列表容器进行上移,所以需要偏移量让它向下移动到可视区域内。
  • 不需要对实际展示列表容器实时进行偏移,仅当要更新列表时再次进行偏移,否则会没有数据滚动效果。

虚拟列表的具体实现

<template>
  <div class="page" ref="page" @scroll="onScroll">
    <!-- 占位 -->
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <!-- 列表展示容器 -->
    <div :style="{ transform: getTransform }">
      <!-- 实际展示的列表 -->
      <div class="item" :key="item.id" v-for="item in showList">
        {{ item.id }}
      </div>
    </div>
  </div>
</template>


<script>
/*eslint-disable*/
let id = 0;
const add = () => {
  let arr = [];
  let len = id + 10;
  let i = id;
  while (i < len) {
    arr.push({ id: i });
    i++;
  }
  id = i;
  return arr;
};

export default {
  name: "virtualList",
  data: () => {
    return {
      totalList: [], //  列表实际数据
      screenHeight: 0, //  容器高度
      itemHeight: 100, //  每个列表项的高度
      start: 0, //  展示列表开始索引
      end: 0, //  展示列表结束索引
      count: 0, //  容器内可展示的列表数量
      startOffset: 0, //  际列表展示容器的偏移量
    };
  },
  computed: {
    //  计算实际列表占用的总高度
    listHeight() {
      return this.totalList.length * this.itemHeight;
    },
    //  计算展示的列表数据
    showList() {
      return this.totalList.slice(this.start, this.end);
    },
    //  计算展示列表容器的偏移量
    getTransform() {
      return `translate3d(0,${this.startOffset}px,0)`;
    },
  },
  mounted() {
    //  mock数据
    this.totalList = [...this.totalList, ...add()];
    //  获取可视区域的高度
    this.screenHeight = this.$refs.page.offsetHeight;
    //  计算可视区域内可展示的列表数量
    this.count = Math.floor(this.screenHeight / this.itemHeight);
    //  计算展示列表结束索引
    this.end = this.start + this.count;
  },
  methods: {
    onScroll() {
      //    当前滚动条滚动的高度
      const scrollTop = this.$refs.page.scrollTop;
      //    计算实际展示列表项的开始索引
      this.start = Math.floor(scrollTop / this.itemHeight);
      //    计算实际展示列表项的结束索引
      this.end = this.start + this.count;
      //    计算偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemHeight);

      //  模拟到底部时,获取新数据场景
      if (
        this.$refs.page.scrollTop + this.$refs.page.clientHeight ===
        this.$refs.page.scrollHeight
      ) {
        this.totalList = [...this.totalList, ...add()];
      }
    },
  },
};
</script>

<style lang="less" scoped>
.page {
  width: 500px;
  height: 500px;
  overflow-y: auto;
  position: relative;

  .placeholder {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    z-index: -1;
  }

  .item {
    height: 100px;
    line-height: 100px;
  }
}
</style>