在前端的场景中,有很多数据无限的下拉列表,比如商品列表和或者聊天记录列表等等。
随着数据的不断加载,页面中可能会渲染出几千几万个相同的元素,这样对性能的损耗是非常大的。
为了解决这一问题,就出现了切片渲染和虚拟列表的概念,这里就来说一下虚拟列表。

虚拟列表是使用技术手段实现长列表渲染的一种方法,操作的原理就好比,就好比电梯和楼层的关系,通过索引来显示需要显示数据,只不过虚拟列表中的电梯是一段,可能同时可以到达多个连续的楼层,比如电梯的长度是3,那就可以同时到达123,234,345,等等。

用代码实现思路如下:

1、给定一个列表元素,使用for循环加载子元素。
2、确定视口可以展示几个元素。
3、截取数组,按照滚动到顶部的位置,计算开始显示数据的索引和结束的索引,结束的索引=开始的索引+视口可以展示的元素个数。
4、滚动条改变时,改变开始的索引和结束的索引。
5、计算填充,让滚动更加丝滑。

以下是代码实现:

<template>
  <div
    ref="listContainer"
    class="list-container"
    @scroll.passive="scrollHandle"
    :style="{
      '--rowHeight': oneHeight + 'px',
    }"
  >
    <!-- 撑开进度条的方法,滚动效果不好 -->
    <!-- <div
      class="phantom"
      :style="{
        height: list.length * oneHeight + 'px',
      }"
    ></div> -->
    <!-- 第二种使用,使用padding撑开进度条,滚动效果好 -->
    <div class="list" ref="list" :style="listBlankPadding">
      <div class="item" v-for="item in showList" :key="item.index">
        第{{ item.value }}项列表
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
    // 单条数据的高度
    oneHeight: {
      type: Number,
      default: 50,
    },
  },
  data() {
    return {
      start: 0,
      containterHeight: 0,
      // 单页面也是的数量
      showNumber: 0,
    };
  },
  computed: {
    end() {
      // 不能最直接取start+showNumber,要判断数据有没有
      return this.list[this.start + this.showNumber]
        ? this.start + this.showNumber
        : this.list.length - 1;
    },

    listBlankPadding() {
      return {
        paddingTop: this.start * this.oneHeight + "px",
        paddingBottom: (this.list.length - this.end) * this.oneHeight + "px",
      };
    },
    // 可视数组
    showList() {
      return this.list.slice(this.start, this.end);
    },
  },

  mounted() {
    this.getListHeight();
    // 改变窗口获取可显示的条数
    window.onresize = this.getListHeight;
  },
  methods: {
    scrollHandle() {
      const marginTop = this.$refs["listContainer"].scrollTop;
      // ~~等于取整操作
      this.start = ~~(marginTop / this.oneHeight);

      // 第一种改变填充进度条时,需要改变改变顶部的位置
      // this.$refs["list"].style.transform = `translateY(${marginTop}px)`;
    },
    getListHeight() {
      this.$nextTick(() => {
        // 头部和底部会有显示两个半条的情况,所以+2
        this.showNumber =
          ~~(this.$refs["listContainer"].offsetHeight / this.oneHeight) + 2;
      });
    },
  },
};
</script>

<style scoped>
.list-container {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  position: relative;
  background: turquoise;
}

.phantom {
  width: 10px;
  z-index: -1;
  position: absolute;
}

.list {
  position: absolute;
  inset: 0;
}

.item {
  height: var(--rowHeight);
}
</style>

最终实现的效果如下所示:


vue虚拟列表


当我们在父组件当中使用时,只需将组件导入,传入list就可以,单行高度不传时默认50px。

<template>
  <div id="app">
    <VirtureList :list="list" />
  </div>
</template>

<script>
import VirtureList from "@/components/virture-list.vue";

export default {
  name: "App",
  components: {
    VirtureList,
  },
  computed: {
    list() {
      return Array(100)
        .fill("")
        .map((item, index) => {
          return {
            value: index,
            index,
          };
        });
    },
  },
};
</script>

<style>
#app {
  height: 100vh;
  width: 100vw;
}
</style>