轻松实现 Vue.js 虚拟滚动列表组件
基于Vue 2 实现一个虚拟列表组件
今天一直在维护一个老功能,全力提升数以万计的图片性能体验,以确保用户获得更优质的享受。把内容整理了一下,顺便发上来。
本文将用轻松的方式向你展示如何用 Vue.js 封装一个虚拟滚动列表组件,让你的应用轻松应对大量数据展示,提升滚动性能。
为什么要用虚拟滚动呢?想象一下,你的应用需要展示成千上万的数据项,这时候直接渲染所有数据项,浏览器可能会吃不消。虚拟滚动的原理很简单,就是只渲染当前可见的那部分列表项,从而节省资源,提升性能。
好,废话不多说,我们来看看这个虚拟滚动列表组件的代码:
<template>
  <div class="virtual-list" @scroll="onScroll">
    <!-- 插槽:列表顶部的内容 -->
    <slot name="before"></slot>
    <!-- spacer div 用于撑开列表的总高度 -->
    <div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }"></div>
    <!-- viewport div 用于包含当前可见的项目 -->
    <div class="virtual-list-viewport" :style="{ transform: 'translateY(' + viewportOffset + 'px)' }">
      <!-- 使用 v-for 渲染可见项目,并为每个项目提供一个插槽 -->
      <slot v-for="(item, index) in visibleItems" :item="item" :index="index + startIndex"></slot>
    </div>
    <!-- 插槽:列表底部的内容 -->
    <slot name="after"></slot>
  </div>
</template>
<script>
    export default {
      name: 'VirtualList',
      props: {
        // items 属性表示要显示的项目数组;
        items: {
          type: Array,
          required: true,
        },
        // itemHeight 属性表示每个项目的高度(必须是固定的);
        itemHeight: {
          type: Number,
          required: true,
        },
        // buffer 属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
        buffer: {
          type: Number,
          default: 5,
        },
      },
      data() {
        return {
          totalHeight: 0,
          viewportOffset: 0,  // 数据表示滚动容器的当前滚动位置
          startIndex: 0,  // 数据表示当前可见项目的起始索引。
          endIndex: 0,  // 数据表示当前可见项目的结束索引。
          isDataReady: false,
        };
      },
      computed: {
        /**
         * @returns {*[]} 返回一个新数组,该数组包含从 startIndex 到 endIndex 的所有项目。
         */
        visibleItems() {
          return this.items.slice(this.startIndex, this.endIndex + 1);
        },
      },
      watch: {
        items() {
          this.updateTotalHeight();
          this.updateVisibleItems();
        },
        visibleItems() {
          this.$emit('change-activeIndex', this.startIndex + 1);
        }
      },
      mounted() {
        this.updateTotalHeight();
        // this.updateVisibleItems();
      },
      methods: {
        // 更新列表的总高度(totalHeight 数据)
        updateTotalHeight() {
          this.totalHeight = this.items.length * this.itemHeight;
        },
        // 计算当前可见项目的范围(startIndex 和 endIndex)
        // 并更新视口偏移(viewportOffset)
        updateVisibleItems() {
          // 获取当前滚动条的位置
          const scrollTop = this.$el.scrollTop;
          // 计算当前视口中可见项目的数量(加上缓冲区项目数量)
          const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) + this.buffer;
          this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
          this.endIndex = Math.min(this.items.length - 1, this.startIndex + visibleCount);
          // 更新视口偏移(viewportOffset)
          this.viewportOffset = this.startIndex * this.itemHeight;
          // 检测是否滚动到底部
          if (this.$el.scrollHeight - scrollTop <= this.$el.clientHeight + this.itemHeight) {
            this.$emit('load-more');
          }
        },
        // 当滚动容器滚动时调用
        onScroll() {
          // 更新可见项目的范围
          this.updateVisibleItems();
        },
      },
    };
</script>
<style scoped>
    .virtual-list {
      overflow-y: auto;
      position: relative;
      height: 100%;
    }
    .virtual-list-spacer {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
    }
    .virtual-list-viewport {
      position: absolute;
      left: 0;
      right: 0;
    }
</style>
现在来逐步解释如何实现 VirtualList 组件:
1. 定义模板结构:
在模板中,我们创建一个带有 virtual-list 类的 div,用作虚拟列表的容器。
我们为其添加一个 @scroll 事件侦听器,以便在滚动时调用 onScroll 方法。
接下来,我们创建一个名为 virtual-list-spacer 的 div,用于撑开容器的高度。
我们还添加了三个插槽:before、默认插槽和 after,用于在列表的顶部、中间和底部渲染内容。
    <template>
      <div class="virtual-list" @scroll="onScroll">
        <!-- 插槽:列表顶部的内容 -->
        <slot name="before"></slot>
        <!-- spacer div 用于撑开列表的总高度 -->
        <div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }"></div>
        <!-- viewport div 用于包含当前可见的项目 -->
        <div class="virtual-list-viewport" :style="{ transform: 'translateY(' + viewportOffset + 'px)' }">
          <!-- 使用 v-for 渲染可见项目,并为每个项目提供一个插槽 -->
          <slot v-for="(item, index) in visibleItems" :item="item" :index="index + startIndex"></slot>
        </div>
        <!-- 插槽:列表底部的内容 -->
        <slot name="after"></slot>
      </div>
    </template>
2. 定义组件属性:
我们需要接收几个属性,以便正确地设置虚拟列表。
items 属性表示要显示的项目数组;
itemHeight 属性表示每个项目的高度(必须是固定的);
buffer 属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
    props: {
        // items 属性表示要显示的项目数组;
        items: {
          type: Array,
          required: true,
        },
        // itemHeight 属性表示每个项目的高度(必须是固定的);
        itemHeight: {
          type: Number,
          required: true,
        },
        // buffer 属性表示要在列表的顶部和底部额外渲染的项目数量,以提高滚动性能。
        buffer: {
          type: Number,
          default: 5,
        },
  },
3. 定义组件数据:
我们需要存储一些数据,以便在组件中跟踪滚动位置和当前可见项目的范围。
viewportOffset 表示数据表示滚动容器的当前滚动位置;
startIndex 和 endIndex 数据表示当前可见项目的起始和结束索引。
    data() {
        return {
          totalHeight: 0,
          viewportOffset: 0,  // 数据表示滚动容器的当前滚动位置
          startIndex: 0,  // 数据表示当前可见项目的起始索引。
          endIndex: 0,  // 数据表示当前可见项目的结束索引。
        };
    },
4. 定义计算属性:
我们需要两个计算属性来确定要显示哪些项目以及撑开容器高度的 spacer div 应具有多大的高度。
visibleItems 计算属性返回一个新数组,返回一个新数组,该数组包含从 startIndex 到 endIndex 的所有项目。
    computed: {
      visibleItems() {
          return this.items.slice(this.startIndex, this.endIndex + 1);
      },
    },
5. 初始化组件:
在 mounted 生命周期钩子中,我们调用 updateTotalHeight 方法,更新列表的总高度(totalHeight 数据)。
    mounted() {
      this.updateTotalHeight();
    },
6. 定义方法:
我们需要定义几个方法来处理滚动事件、计算可见项目范围等。
- 
onScroll方法:在滚动事件发生时调用。onScroll(e) { // 更新可见项目的范围 this.updateVisibleItems(); }, - 
updateVisibleItems方法:计算当前可见项目的范围(startIndex 和 endIndex),并更新视口偏移(viewportOffset)。// 计算当前可见项目的范围(startIndex 和 endIndex) // 并更新视口偏移(viewportOffset) updateVisibleItems() { // 获取当前滚动条的位置 const scrollTop = this.$el.scrollTop; // 计算当前视口中可见项目的数量(加上缓冲区项目数量) const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) + this.buffer; this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer); this.endIndex = Math.min(this.items.length - 1, this.startIndex + visibleCount); // 更新视口偏移(viewportOffset) this.viewportOffset = this.startIndex * this.itemHeight; // 检测是否滚动到底部 if (this.$el.scrollHeight - scrollTop <= this.$el.clientHeight + this.itemHeight) { this.$emit('load-more'); } }, 
7. 添加样式:
    <style scoped>
      .virtual-list {
        overflow-y: auto;
        position: relative;
        height: 100%;
      }
      .virtual-list-spacer {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
      }
      .virtual-list-viewport {
        position: absolute;
        left: 0;
        right: 0;
      }
    </style>
至此,我们已经完成了 VirtualList 组件的实现。现在,您可以将该组件用于任何需要虚拟滚动的场景,以提高大型列表的性能。
8. 使用示例
    <template>
  <div>
    <virtual-list
      :items="users"
      :item-height="50"
      :buffer="3"
      @load-more="loadMore"
    >
      <template #before>
        <div class="list-header">User List</div>
      </template>
      <template v-slot="{ item, index }">
        <div style="height: 40px;">
          {{item.name}}
        </div>
      </template>
      <template #after>
        <div class="list-footer" v-if="loading">Loading...</div>
      </template>
    </virtual-list>
  </div>
</template>
<script>
import VirtualList from '@/views/components/VirtualList';
export default {
  components: {
    VirtualList,
  },
  data() {
    return {
      users: [
        // 填充初始用户数据,例如:
        { name: 'User 1' },
        { name: 'User 2' },
        { name: 'User 3' },
        // ...
      ],
      loading: false,
    };
  },
  mounted() {
    this.loadMore()
  },
  methods: {
    loadMore() {
      if (this.loading) {
        return;
      }
      this.loading = true;
      // 模拟延迟加载
      setTimeout(() => {
        for (let i = 0; i < 20;
             i++) {
          this.users.push({ name: `User ${this.users.length + 1}` });
        }
        this.loading = false;
        console.log(this.users)
      }, 1000);
    },
  },
};
</script>
<style>
  .virtual-list {
    height: 400px!important;
  }
  .list-header {
    background-color: #f0f0f0;
    padding: 10px;
    text-align: center;
  }
  .list-footer {
    padding: 10px;
    text-align: center;
  }
</style>

转载自:https://juejin.cn/post/7216526817372749881