vue中使用虚拟列表,封装虚拟列表

1.技术栈:vue3

2.原理:通过滚动,展示可视区域数据 ,vue模版里可传入元素宽高,支持多列虚拟滚动。

实现了数据多的情况下页面卡顿的情况,优化性能。

3.虚拟组件手动封装代码:

javascript 复制代码
<template>
  <div class="virtual-list" ref="listContainer" @scroll="onScroll">
    <div
      class="virtual-list-content"
      :style="{
        height: totalHeight + 'px',
        display: flages ? 'flex' : '',

        flexWrap: 'wrap',
        alignContent: 'flex-start',
      }"
    >
      <div
        v-for="(item, index) in visibleItems"
        :key="item.id"
        :style="{
          width: widthData + 'px',
          height: itemHeight + 'px',
          transform: `translateY(${offsetY}px)`,
          marginTop: type == 1 ? '' : '10px',
        }"
        class="virtual-list-item"
      >
        <slot :item="item" :index="index"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick, onActivated, getCurrentInstance } from "vue";

const props = defineProps({
  items: {
    type: Array,
    required: true,
  },
  itemHeight: {
    type: Number,
    default: 30,
  },
  flagFlex: {
    type: Boolean,
    default: false,
  },
  type: {
    type: Number,
    default: 0,
  },
  widthData: {
    type: String,
    default: "190",
  },
});

const listContainer = ref(null);
const startIndex = ref(0);
const visibleCount = ref(0);
const offsetY = ref(0);
const savedScrollTop = ref(0);
const resizeObserver = ref(null); // 保存 ResizeObserver 实例
const isMounted = ref(false); // 标记组件是否已挂载
const widthData = computed(() => props.widthData);

// 计算列数的函数
const calculateColumnCount = () => {
  const container = listContainer.value;
  let columnCount = 1;
  if (container) {
    columnCount = Math.floor(container.clientWidth / parseInt(props.widthData, 10));
    columnCount = Math.max(columnCount, 1);
  }
  return columnCount;
};

const totalHeight = computed(() => {
  const columnCount = calculateColumnCount();
  const itemsPerColumn = Math.ceil(props.items.length / columnCount);
  
  return itemsPerColumn * props.itemHeight;
});

const flages = computed(() => props.flagFlex);
const types = computed(() => props.type);

const calculateVisibleCount = () => {
  const container = listContainer.value;
  
  if (container) {
    const containerHeight = container.clientHeight;
    
    const columnCount = calculateColumnCount();
    visibleCount.value = Math.ceil(containerHeight / props.itemHeight) * columnCount + columnCount;
  }
};

const visibleItems = computed(() => {
  const endIndex = startIndex.value + visibleCount.value;
  
  return props.items.slice(startIndex.value, endIndex);
});

const onScroll = () => {
  
  const container = listContainer.value;
  if (container) {
    const scrollTop = container.scrollTop;
    const columnCount = calculateColumnCount();
    startIndex.value = Math.floor(scrollTop / props.itemHeight) * columnCount;
    
    offsetY.value = scrollTop - (scrollTop % props.itemHeight);
    savedScrollTop.value = scrollTop;
  }
};

// 观察容器大小变化的函数
const observeContainerSize = () => {
  const container = listContainer.value;
  if (container) {
    const observer = new ResizeObserver(() => {
      calculateVisibleCount();
      // 触发 totalHeight 的重新计算
      // totalHeight.value;
    });
    observer.observe(container);
    return observer;
  }
  return null;
};

onMounted(() => {
  // 检查组件实例是否存在
  const instance = getCurrentInstance();
  if (!instance) {
    console.warn('VirtualList: No active component instance in onMounted');
    return;
  }
  
  isMounted.value = true;
  
  // 直接执行初始化,Vue 会确保在 DOM 挂载后执行
  try {
    if (listContainer.value) {
      calculateVisibleCount();
      // 监听 resize 事件
      window.addEventListener("resize", calculateVisibleCount);
      // 监听容器自身大小变化
      resizeObserver.value = observeContainerSize();
    } else {
      // 如果容器还未挂载,等待下一个 tick
      nextTick(() => {
        // 再次检查组件实例和挂载状态
        const currentInstance = getCurrentInstance();
        if (currentInstance && isMounted.value && listContainer.value) {
          calculateVisibleCount();
          window.addEventListener("resize", calculateVisibleCount);
          resizeObserver.value = observeContainerSize();
        }
      });
    }
  } catch (error) {
    // 如果组件已经被卸载,忽略错误
    console.warn('VirtualList onMounted error:', error);
  }
});

// 在组件卸载时移除监听
onUnmounted(() => {
  isMounted.value = false;
  window.removeEventListener("resize", calculateVisibleCount);
  if (resizeObserver.value && listContainer.value) {
    resizeObserver.value.unobserve(listContainer.value);
    resizeObserver.value.disconnect();
    resizeObserver.value = null;
  }
});

onActivated(() => {
  const container = listContainer.value;
  if (container) {
    if (savedScrollTop.value !== 0) {
      container.scrollTop = savedScrollTop.value;
    }
    calculateVisibleCount();
    const columnCount = calculateColumnCount();
    startIndex.value = Math.floor(savedScrollTop.value / props.itemHeight) * columnCount;
    offsetY.value = savedScrollTop.value - (savedScrollTop.value % props.itemHeight);
  }
});

// 监听组件显示状态的变化
watch(
  () => visibleItems.value,
  () => {
    const container = listContainer.value;
    if (container) {
      if (savedScrollTop.value !== 0) {
        container.scrollTop = savedScrollTop.value;
      }
      calculateVisibleCount();
      const columnCount = calculateColumnCount();
      startIndex.value = Math.floor(savedScrollTop.value / props.itemHeight) * columnCount;
      offsetY.value = savedScrollTop.value - (savedScrollTop.value % props.itemHeight);
    }
  }
);
</script>

<style scoped>
.virtual-list {
  overflow-y: auto;
  width: 100%;
  height: 100%;
  position: relative;
  background-color: rgb(40, 44, 53);
}

.virtual-list::-webkit-scrollbar {
  width: 5px;
}

.virtual-list::-webkit-scrollbar-thumb {
  background-color: #2ecc71;

  border-radius: 2px;
}

.virtual-list-content {
  position: relative;
  /* justify-content: space-around; */
}

.virtual-list-item {
  box-sizing: border-box;
}
</style>

4.在模版中使用:VirtualList的template标签里的自己根据情况弄自己项目的:ItemComponent需要传入

javascript 复制代码
 <VirtualList :items="checkedCities" :itemHeight="50" :itemComponent="ItemComponent" :flagFlex="true" :type="3"  :widthData="'200'">
            <template #default="{ item, index }">
              <div class="otherxijieevery" v-if="findData(item)">
                <div style="display: flex">
                  <img :src="findData(item).avatar || userInfoStore.imgMAx" alt="" />
                  <div class="otherxijieevery_every_two0">
                    <p>
                      {{ findData(item).nickname }}
                    </p>
                    <p>群人数&nbsp;&nbsp;{{ findData(item).total_member.length }}</p>
                  </div>
                  <div
                    style="
                      font-size: 11px;
                      position: absolute;
                      top: 10px;
                      right: 5px;
                      width: 15px;
                      height: 15px;
                      border-radius: 50px;
                      background-color: rgb(40, 44, 53);
                      border: 1px solid #cdd0d692;
                      text-align: center;
                      line-height: 18px;
                      color: white;
                    "
                    @click="clearOne(item)"
                  >
                    <el-icon>
                      <Minus />
                    </el-icon>
                  </div>
                  <div style="font-size: 11px; color: #e6a23c; position: absolute; bottom: 0; right: 5px">
                    {{ findData(item).is_manager == 1 ? "@群主" : "" }}
                  </div>
                </div>
              </div>
            </template>
          </VirtualList>








//参数ItemComponent 

const ItemComponent = {

  template: '<div class="custom-item"><slot></slot></div>',

};
相关推荐
passerma7 分钟前
解决qiankun框架子应用打包后css里的图片加载404失败问题
前端·微前端·qiankun
Aliex_git19 分钟前
性能优化 - Vue 日常实践优化
前端·javascript·vue.js·笔记·学习·性能优化
董世昌4122 分钟前
添加、删除、替换、插入元素的全方法指南
java·开发语言·前端
qq_3168377528 分钟前
Element-Plus el-table lazy 自动更新子列表
前端·vue.js·elementui
xiaoxue..30 分钟前
把大模型装进自己电脑:Ollama 本地部署大模型完全指南
javascript·面试·node.js·大模型·ollama
Mr.app1 小时前
VUE:Ul列表内容自动向上滚动
vue.js
林恒smileZAZ1 小时前
Electron 的西天取经
前端·javascript·electron
这就是佬们吗1 小时前
告别 Node.js 版本冲突:NVM 安装与使用全攻略
java·linux·前端·windows·node.js·mac·web
IT_陈寒1 小时前
2024年JavaScript开发者必备的10个ES13新特性实战指南
前端·人工智能·后端
Miketutu1 小时前
Flutter - 布局
开发语言·javascript·ecmascript