定高与不定高虚拟列表

前言

        在日常代码开发过程中,总会遇到大数据量的问题,当我们需要加载显示几千上万的数据的时候,如果我们是一次性渲染,那肯定就会出现严重的卡顿现象,这对用户体验是非常差的,也会让我们的项目,可用性大大降低,为此我们可以使用虚拟列表这个解决方案,只显示我们可视区域内可展示的数据量,这样就大大降低了页面卡顿的概率。

定高虚拟列表

        定高虚拟列表就是虚拟列表的每一行的高度都是固定的,所以做起来也比较方便,一个可视的容器,里面包括这一个用于撑开让可视区域出现滚动条的元素,加列表元素,通过滚动的距离除于每一行的高,得到开始坐标 startIndex, 通过开始坐标加上 可是容器的高度除于每一行的高得到结束下表 endIndex, 然后可视区域展示的数据 就通过startIndex 与 endIndex 去截取,词不达意,直接上代码

效果

代码

<template>
  <!-- 虚拟列表可视区域 -->
  <div class="virtual-list" :style="{ height }" @scroll="onScroll">
    <!-- 撑起出现滚动条的元素 -->
    <div class="total-height" :style="{ height: `${totalHeight}px` }"></div>
    <!-- 虚拟列表内容区域 -->
    <div class="virtual-body" :style="{ transform: `translateY(${scrollY}px` }">
      <div
        class="virtual-item"
        v-for="item in showList"
        :key="item.id"
        :style="{ height: `${itemHeight}px` }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from "vue";

const props = defineProps({
  // 列表所有数据
  data: {
    type: Array,
    default: () => [],
  },
  // 每一项的高度
  itemHeight: {
    type: Number,
    default: 50,
  },
  // 可视区域的高度
  height: {
    type: Number,
    default: 300,
  },
});

// 滚动距离/虚拟列表体移动距离
const scrollY = ref(0);
// 要展示在可视区域的数据
const showList = computed(() => {
  // 计算可视区域起始索引
  const startIndex = Math.ceil(scrollY.value / props.itemHeight);
  // 计算可视区域结束索引
  const endIndex = Math.ceil((scrollY.value + props.height) / props.itemHeight);
  // 截取可视区域数据
  return props.data.slice(startIndex, endIndex);
});
// 列表所有数据的总高度
const totalHeight = computed(() => props.data.length * props.itemHeight);

const onScroll = (e) => {
  // 获取滚动距离
  scrollY.value = e.target.scrollTop;
};

watch(
  () => props.data,
  () => {
    // 数据更新后,重置滚动距离
    scrollY.value = 0;
  }
);
</script>

<style scoped>
.virtual-list {
  overflow-y: auto;
  width: 100%;
  position: relative;
  border: dashed 1px orange;
}

.total-height {
  width: 100%;
  position: absolute;
}

.virtual-body {
  position: relative;
  width: 100%;
}

.virtual-item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-bottom: dashed 1px orange;
}
</style>

由于定高虚拟列表比较简单就直接上代码了,代码中也有相应的注释

使用的地方

<template>
  <div class="container">
    <div>
      <h1>定高虚拟列表</h1>
      <FixedHighVirtualListVue :height="600" :itemHeight="50" :data="data" />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import FixedHighVirtualListVue from "./components/fixed-high-virtual-list/fixed-high-virtual-list.vue";

const data = ref([]);

onMounted(() => {
  for (let index = 0; index < 10000; index++) {
    data.value.push({
      index,
      name: `name-${index}-${"hello world".repeat(6)}`,
    });
  }
});
</script>

<style scoped>
.container {
  display: flex;
  div {
    width: 500px;
    margin-left: 10px;
  }
}
</style>

不定高虚拟列表 

        不定高虚拟列表,就是每一行的高度不确定的,要等可视区域的数据渲染完毕之后,才知道每一项的高度,所以这块会有一个预设的步骤,就是还未在可视区域显示过的元素的高同意预设一个值,等元素渲染之后,再根据实际渲染的值,更新我们的预设值,从而再计算出startInex 与 endIndex, 进而得到要渲染的数据。

效果

可以看到每一行的高度都是不太一样的,具体怎么实现,可以继续往下看

html 结构

 

<template>
  <!-- 虚拟列表可视区域 -->
  <div
    class="virtual-list"
    :style="{ height: `${height}px` }"
    @scroll="onScroll"
  >
    <!-- 撑起出现滚动条的元素 -->
    <div
      class="total-height"
      :style="{ height: `${state.totalHeight}px` }"
    ></div>
    <!-- 虚拟列表内容区域 -->
    <div
      class="virtual-body"
      :style="{ transform: `translateY(${state.scrollY}px` }"
    >
      <!-- item 具有 index 及 name属性 与 VirtualRowVue 的props一致可以v-bind直接绑定 item-->
      <VirtualRowVue
        v-for="item in state.showList"
        v-bind="item"
        @changeSize="onChangeSize"
      />
    </div>
  </div>
</template>

实现过程 

第一 监听传进来的列表数据

watch(props.data, () => {
  // 渲染预估搞定的虚拟列表
  refreshShowList();
});

 当我们刚开始渲染列表的时候,就会调用refreshShwoList 更新我们展示的内容

第二 刷新列表

// 刷新可视区域的元素
const refreshShowList = () => {
  // 刷新总高度
  refreshTotalHeight();

  // 获取开始结束索引
  const [startIndex, endIndex] = getRangeIndex();

  // 遍历赋值可视区域数据
  state.value.showList = props.data.slice(startIndex, endIndex + 1);
};

这里主要是,计算出,撑开滚动条的元素的高度,也就是所有元素的高度总和,然后计算出开始与结束下表用于截取可视区域的数据

第三 刷新总高度

// 计算总高度
const refreshTotalHeight = () => {
  // 获取已记录的最后一个元素
  const { mapObj, lastIndex } = displayedData;
  const lastItem = mapObj[lastIndex] || {
    offset: 0,
    height: 0,
  };
  // 计算已记录的总高度
  const displayedTotalHeight = lastItem.offset + lastItem.height;
  // 计算未记录的总高度
  const unDisplayedTotalHeight =
    (props.data.length - lastIndex - 1) * props.estimatedHeight;
  // 得到总高度
  state.value.totalHeight = displayedTotalHeight + unDisplayedTotalHeight;
};

这里主要就是计算已经出现过在可视区域的元素的高度和(每个元素的高度都是实际的高度)与未出现过在可视区域的元素的高度和(每个元素的高度是预设的)mapObj 就是 用来记录出现过在可视区域的元素的偏移量 与 自身高度的对象

第四 计算开始与结束下标

// 获取开始结束索引
const getRangeIndex = () => {
  // 获取可视区域开始索引
  const startIndex = getStartIndex();
  // 获取可视区域结束索引
  const endIndex = getEndIndex(startIndex);

  return [Math.max(startIndex, 0), Math.min(endIndex, props.data.length - 1)];
};

第五 计算开始下标

// 获取可视区域开始索引
const getStartIndex = () => {
  // 找到偏移量大于等于滚动距离的第一个元素
  let startIndex = 0;
  for (let i = 0; i < props.data.length; i++) {
    // 获取每一项的信息, 如果没有就使用预估高度
    const item = getItemInfo(i);
    if (item.offset >= state.value.scrollY) {
      startIndex = i;
      break;
    }
  }
  return startIndex;
};

当我们计算出偏移量大于等于滚动距离的时候,这时候得到的就是可视区域开始元素的下标了

第五步 计算结束下标

// 获取可视区域结束索引
const getEndIndex = (startIndex) => {
  // 获取开始下标的项
  const startItem = getItemInfo(startIndex);
  // 计算最后的下标的偏移量
  const endOffset = startItem.offset + props.height;

  // 遍历计算结束下标
  let endIndex = startIndex;
  let offset = startItem.offset;
  while (offset < endOffset && endIndex < props.data.length) {
    const item = getItemInfo(++endIndex);
    offset += item.height;
  }

  return endIndex;
};

结束下标,就得通过开始下标的偏移量 + 可视区域的高度去计算了,当找到元素的偏移量(offset)大于等于他俩(开始下标的偏移量 、可视区域的高度)的和的时候,那这个下标就是结束下标了

第六 获取每一个元素的信息

// 获取每一项的信息, 如果没有就使用预估高度
const getItemInfo = (index) => {
  const { mapObj, lastIndex } = displayedData;
  // 如果是往下滚就是需要计算新的开始偏移量
  if (index > lastIndex) {
    // 第一项的时候 mapObj[lastIndex] 是 undefined 需要给个默认值
    const lastItem = mapObj[lastIndex] || {
      offset: 0,
      height: 0,
    };

    // 计算新的开始偏移量没有记录的项统计使用预估高度
    let offset = lastItem.offset + lastItem.height;
    for (let i = lastIndex + 1; i <= index; i++) {
      mapObj[i] = {
        offset,
        height: props.estimatedHeight,
      };
      offset += props.estimatedHeight;
    }

    // 更新最后一个索引
    displayedData.lastIndex = index;
  }

  return mapObj[index];
};

mapObj 就是保存在可视区域出现过的元素的对象, 如果元素在可视区域出现过,就直接通过mapObj[index] 返回,如果没有出现过,代表是向下滚动的,这种元素就得保存一下,高度时估计高度,同时保存出现在可视区域的最后一个元素的下标,当这些预设高度的元素渲染那完毕之后,都会触发一下 onChangeSize 事件 用于更新maoObj 中高度及偏移量还不是实际值的数据

第七 onChangeSize函数

// 子元素大小改变
const onChangeSize = ({ index, height }) => {
  // 更新子元素高度
  const item = getItemInfo(index);
  item.height = height;

  // 更新子元素偏移量
  const { mapObj, lastIndex } = displayedData;
  let offset = item.offset + item.height;
  for (let i = index + 1; i <= lastIndex; i++) {
    const curItem = getItemInfo(i);
    curItem.offset = offset;
    offset += curItem.height;
  }
};

第八 子组件

<template>
  <!-- 虚拟列表可视区域 -->
  <div class="virtual-row" ref="virtualRowRef">{{ name }}</div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from "vue";

const props = defineProps({
  // 每一行展示的内容
  name: {
    type: String,
    default: "",
  },
  // 数据在总列表的下标
  index: {
    type: Number,
    default: 0,
  },
});

const emits = defineEmits(["changeSize"]);

const virtualRowRef = ref(null);
let observe = null;
onMounted(() => {
  // 监视元素变化
  observe = new ResizeObserver(() => {
    emits("changeSize", {
      index: props.index,
      height: virtualRowRef.value.offsetHeight,
    });
  });

  // 监视行元素
  observe.observe(document.querySelector(".virtual-row"));
});

onUnmounted(() => {
  // 销毁监听
  observe?.disconnect?.();
});
</script>

<style>
.virtual-item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-bottom: dashed 1px blueviolet;
}
</style>

 子组件相对简单,主要就是监听元素是否在可视区域,是的话就出触发 父组件的 onChangeSize 事件,把元素标识 及元素高度传过去,进而更新mapObj中的数据,渲染出正确的内容

以上便是所哟内容,作为自己的学习笔记记录,如果恰好能帮到你,那再好不过了,代码地址在这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZL随心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值