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>',

};
相关推荐
黑臂麒麟2 小时前
Electron for OpenHarmony 跨平台实战开发:Electron 文件系统操作实战
前端·javascript·electron·openharmony
wordbaby2 小时前
Tanstack Router 文件命名速查表
前端
用户7227868123442 小时前
Vue2中能否实现输入中文自动转化为拼音, 且不带音调
vue.js
1024肥宅2 小时前
工程化工具类:模块化系统全解析与实践
前端·javascript·面试
软件技术NINI2 小时前
如何学习前端
前端·学习
weixin_422555422 小时前
ezuikit-js官网使用示例
前端·javascript·vue·ezuikit-js
鱼鱼块2 小时前
从零搭一个 Vue 小家:用 Vite + 路由轻松入门现代前端开发
vue.js·面试·前端框架
梓仁沐白2 小时前
CSAPP-Attacklab
前端
郑州光合科技余经理2 小时前
海外国际版同城服务系统开发:PHP技术栈
java·大数据·开发语言·前端·人工智能·架构·php