虚拟滚动列表

平常工作中,页面中展示一个列表式是比较常见的需求,如果数据集很大,通常会对数据进行分页,每次只请求和展示一部分数据。如果,想一次性将所有数据渲染到表格中,这时候使用普通的方式则行不通了,这是因为一次性创建了太多的DOM节点,从而导致卡死。这时,可以采用虚拟滚动的方式来实现效果。

何为虚拟滚动?

比如有1K条数据,但是我们可以看到的部分可能只有几十条,所以,那些我们看不到的dom节点,完全没有必要去渲染。所以,虚拟滚动就是只渲染可视部分的dom节点,在滚动的时候,不断地改变可视区域的值即可。

实现思路:

1、首先,需要获取可视区域的起始索引向下取整,然后获取可视区域的结束索引向上取整。

2、获取到了起始和结束索引,就可以计算出滚动部分上部分填充区域的高度和下部分填充区域的高度

3、在每次滚动的时候去动态的计算上面的这些值,更新到dom中即可

相关代码如下

<div class="wrapper" ref="wrapper" @scroll="handleScroll" :style="{ height: height + 'px' }">
  <div ref="w">
    <div :style="{ height: `${shimTopOffset}px` }"></div>
    <div
      class="item"
      v-for="item in showList"
      :key="item.index"
      :style="{
        height: `${itemHeight}px`,
        color: item.color,
      }"
    >
      {{ item.index }}
    </div>
    <div :style="{ height: `${shimBottomOffset}px` }"></div>
  </div>
</div>
export default {
  data() {
    return {
      height: 500,
      itemHeight: 50,
      data: Array.from({ length: 6000 }).map((_, index) => ({
        index,
        color: `#${Math.random()
          .toString(16)
          .substr(2, 6)}`,
      })),
      showList: [],
      shimTopOffset: 0,
      shimBottomOffset: 0,
    };
  },
  mounted() {
    this.update(0);
    this.$nextTick(() => {
      this.maxHeight = this.$refs.w.offsetHeight;
    });
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.wrapper.scrollTop;
      if (scrollTop >= 0 && scrollTop + this.height <= this.maxHeight) {
        window.requestAnimationFrame(() => {
          this.update(scrollTop);
        });
      }
    },
    update(scrollTop) {
      const visibleStart = scrollTop;
      const visibleEnd = scrollTop + this.height;
      this.showList = this.getShowList(visibleStart, visibleEnd, this.data);
    },
    getShowList(start, end, data) {
      if (start < end) {
        const lo = this.getStartIndex(start, this.itemHeight);
        const hi = this.getEndIndex(end, this.itemHeight);
        this.shimTopOffset = lo >= 0 ? lo * this.itemHeight : 0;
        this.shimBottomOffset = hi >= 0 ? (data.length - hi) * this.itemHeight : 0;
        return data.slice(lo, hi);
      } else {
        this.shimTopOffset = 0;
        this.shimBottomOffset = 0;
        return [];
      }
    },
    getStartIndex(s, itemHeight) {
      const startIndex = ~~(s / itemHeight);
      return startIndex >= 0 ? startIndex : 0;
    },
    getEndIndex(e, itemHeight) {
      const endIndex = Math.ceil(e / itemHeight);
      return endIndex <= this.data.length ? endIndex : this.data.length;
    },
  },
  watch: {
    data: {
      handler(newVal, oldVal) {
        if (oldVal) {
          this.$nextTick(() => {
            this.$refs.wrapper.scrollTop = 0;
            this.handleScroll();
          });
        }
      },
      immediate: true,
    },
    itemHeight() {
      this.handleScroll();
    },
  },
};

效果如下:

See the Pen
eYYobKd
by Leevare (@leevare)
on CodePen.

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注