likes
comments
collection
share

轻松实现 Vue.js 虚拟滚动列表组件

作者站长头像
站长
· 阅读数 23

基于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 表示数据表示滚动容器的当前滚动位置;

startIndexendIndex 数据表示当前可见项目的起始和结束索引。

    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>

轻松实现 Vue.js 虚拟滚动列表组件

转载自:https://juejin.cn/post/7216526817372749881
评论
请登录