likes
comments
collection
share

web技术分享| 虚拟列表实现

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

针对过多数据列表展示造成过多节点渲染使页面卡死或卡顿,特地封装一个简易的虚拟列表,大家可在此基础上进行针对修改组件基于 vue3 + element plus + ts + tailwindcss 开发

设计思路

分为三部分:

  • 父容器占位;
  • 一个子容器展示通过滚动条计算的数据相当于真实渲染节点;
  • 一个子容器作为虚拟列表不渲染列表提供列表真实高度显示滚动条

实现

页面结构

  <el-scrollbar
    class="scrollbar_custom px-4"
    :height="virtualRecord.height"
    @scroll="handleScroll"
  >
    <!-- 虚拟高度 -->
    <div
      class="-z-10 absolute inset-0"
      :style="{ height: virtualRecord.virtualHeight + 'px' }"
    ></div>
    <!-- 真实列表 -->
    <ul
      :class="['absolute inset-0', prop.customClass]"
      :style="{ transform: `translateY(${virtualRecord.offset}px)` }"
    >
      <li
        v-for="(item, index) in virtualRecord.visibleData"
        :key="index + '_'"
        :style="{
          height: virtualRecord.itemHeight + 'px',
        }"
        class="hover:bg-anyrtc-gray_8 text-sm"
      >
        <!-- 插槽 -->
        <slot :item="item"></slot>
      </li>
    </ul>
  </el-scrollbar>

样式

修改 element plus 的 scrollbar 样式

.scrollbar_custom {
 :deep(.el-scrollbar__wrap) {
  .el-scrollbar__view {
    @apply relative;
  }
}

逻辑

组件所需参数

const prop = defineProps({
  // 自定义类名
  customClass: String,
  // 相关配置
  option: {
    type: Object,
    default: () => {
      return {};
    },
  },
  listData: {
    type: Array,
    default: () => {
      return [];
    },
  },
});

组件内部定义

// 组件记录(默认)
const virtualRecord = reactive({
  height: 400,
  // 展示几个
  visibleCount: 16,
  // // 刷新频率
  timeout: 4,
  // // 行高
  itemHeight: 40,
  // translateY偏移量
  offset: 0,
  // 虚拟占位高度
  virtualHeight: 300,

  // 记录滚动高度
  recordScrollTop: 0,

  dataList: [] as any[],
  // 可展示的数据
  visibleData: [] as any[],
});

// 合并配置
const mergeFn = () => {
  virtualRecord.height = prop.option.height || 400;
  virtualRecord.dataList = JSON.parse(JSON.stringify(prop.listData));
  virtualRecord.itemHeight = prop.option.itemHeight || 40;
  virtualRecord.timeout = prop.option.timeout || 4;
  // // 虚拟高度
  virtualRecord.virtualHeight = prop.listData.length * virtualRecord.itemHeight;

  // 展示数量
  virtualRecord.visibleCount = Math.ceil(
    virtualRecord.height / virtualRecord.itemHeight
  );
};

滚动计算

let lastTime = 0;
const handleScroll = (scroll: { scrollTop: number }) => {
  let currentTime = +new Date();
  if (currentTime - lastTime > virtualRecord.timeout) {
    virtualRecord.recordScrollTop = scroll.scrollTop;
    updateVisibleData(scroll.scrollTop);
    lastTime = currentTime;
  }
};
const updateVisibleData = (scrollTop: number) => {
  let start =
    Math.floor(scrollTop / virtualRecord.itemHeight) -
    Math.floor(virtualRecord.visibleCount / 2);
  start = start < 0 ? 0 : start;
  const end = start + virtualRecord.visibleCount * 2;
  virtualRecord.visibleData = virtualRecord.dataList.slice(start, end);
  virtualRecord.offset = start * virtualRecord.itemHeight;
};

列表信息变更

watch(
  () => {
    return [prop.listData, prop.option];
  },
  ([newData, newHeight]) => {
    if (newData) {
      // 合并数据
      mergeFn();
      // 更新视图
      updateVisibleData(virtualRecord.recordScrollTop);
    }
  },
  {
    immediate: true,
  }
);

使用

<VirtualItem :option={height:'占位高度'} :list-data="列表" >
  <template #default="{ item }">
   {{ item }}
  </template>
</VirtualItem>

web技术分享| 虚拟列表实现