虚拟列表学习

虚拟列表

就是只渲染当前可视区的内容,通过这种方式减少DOM的数量

完整列高度 = 每一项高度 × 总项数

可视区域高度 = 屏幕可见高度

滚动高度(scrollHeight) = 完整列表高度

当前滚动距离 = 已滚动的距离

基础版本的虚拟列表

html 复制代码
<template>
  <div class="dashboard-container">
    <div ref="container" class="virtual-container" @scroll="handleScroll">
      <div
        class="placeholder"
        :style="{ height: placeholderHeight + 'px' }"
      ></div>
      <div
        class="list-content"
        :style="{ transform: `translateY(${offsetHeight}px)` }"
      >
        <div
          v-for="item in visibleList"
          :key="item.id"
          class="list-item"
          :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }"
        >
          {{ item.id }}
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
const startIndex = ref(0); //可视区渲染数组的开始下标
const endIndex = ref(0); //可视区渲染数组的结束下标
const listData = ref(
  new Array(1000).fill({}).map((item, index) => ({ id: index }))
); //生成1000条要渲染的数据
const itemHeight = ref(50); //确定每一项的高度
const container = ref(null); //内容区的ref
const offsetHeight = ref(0); //偏移量
const placeholderHeight = ref(itemHeight.value * listData.value.length); //占位元素的高度
const clientHeight = ref(0); //内容区的高度
const visibleList = computed(() => {
   return listData.value.slice(
    startIndex.value,
    Math.min(endIndex.value, listData.value.length)
  );
}); //计算属性得到要渲染的数组
const visibleCount = computed(() => {
  return Math.ceil(container.value.clientHeight / itemHeight.value);
}); //计算属性 获取当前可见区域能渲染的元素的个数
const handleScroll = (e) => {
  // 滚动变化后重新结算新的开始和结束下标
  startIndex.value = Math.floor(e.target.scrollTop / itemHeight.value);
  endIndex.value = startIndex.value + visibleCount.value;
  offsetHeight.value = startIndex.value * itemHeight.value;
};
onMounted(() => {
  // 初始化值
  startIndex.value = 0;
  endIndex.value = startIndex.value + visibleCount.value;
  clientHeight.value = container.value.clientHeight;
});
</script>
<style lang="scss" scoped>
.virtual-container {
  position: relative;
  overflow-y: scroll;
  width: 300px;
  height: 498px;
  background-color: #baf;
  margin: 50px auto 0;
  .list-content {
    width: 100%;
    position: absolute;
    left: 0;
    top: 0;
    .list-item {
      box-sizing: border-box;
      border: 1px solid #ddd;
      text-align: center;
      width: 100%;
    }
  }
}
</style>
visibleCount计算为什么要用Math.ceil
js 复制代码
const visibleCount = computed(() => {
  return Math.ceil(container.value.clientHeight / itemHeight.value);
}); 

假设容器的高度是220px,每项的高度是50px;

计算:visibleCount = 220 / 50 = 4.4

如果使用Math.floor(4.4)得到4,就只能显示4个项目,但是4个项目的高度是: 4×50=200,容器底部会有20px的空白,实际上这20px可以显示第五个项目的部分内容,所以需要显示5个项目才能填满容器的可视区,所以就得使用Math.ceil(4.4) = 5,这样可视区就能渲染五个项目,虽然第五个项目只有部分可见,但是这样可以保证容器被填满,不会出现空白

startIndex为什么是Math.floor
js 复制代码
startIndex = Math.floor(scrollTop / itemHeight.value)

假设

项目高度: 50px

容器高度:200px(可显示4个项目)

总项目数:1000个

容器滚动高度(scrollTop): 125px

startIndex计算:startIndex= 125 / 50 = 2.5

如果是Math.ceil: Math.ceil(125 / 50)=3,本来滚动125px的时候,可视区域的顶部位于项目2的中间(如下:项目2的Y轴位置是100px- 150px),现在开始索引为3,那就是从索引为3的位置开始渲染,项目2就不会渲染,这就会导致可视区域顶部出现空白(因为项目2 的部分内容没有被渲染)

所以为了保证任何部分再可见区域内的项目都被渲染要用Math.floor

项目坐标图(y轴位置):

项目0: 0-50px

项目1: 50-100px

项目2: 100-150px

项目3: 150-200px

项目4: 200-250px

Math.ceil顶部就会出现空白

总结 :在虚拟列表中计算开始索引使用Math.floor,计算可视区域的可见数量Math.ceil

visibleList截取为什么要用Math.min
js 复制代码
listData.value.slice(
    startIndex.value,
    Math.min(endIndex.value, listData.value.length)
  );

防止数组越界

比如现在这里有1000条数据,容器可显示10条,当滚动到第995条附近的时候,此时startIndex = 995,endIndex = 995 + 10 = 1005超过了数据总量,所以需要用Math.min

上面写法存在的问题

上面那样写在快速滚动的时候容器底部会出现空白,需要通过设置上下缓冲区来解决,也就是在前后多取几项,比如可视区可以渲染10项,但是实际取的数组的长度是,上面缓冲区10项 + 可视区10项 + 下面缓冲区的10项,数组一共要取20项,这样滚动的时候就不会出现空白了

html 复制代码
<template>
  <div class="dashboard-container">
    <div ref="container" class="virtual-container" @scroll="handleScroll">
      <div
        class="placeholder"
        :style="{ height: placeholderHeight + 'px' }"
      ></div>
      <div
        class="list-content"
        :style="{ transform: `translateY(${offsetHeight}px)` }"
      >
        <div
          v-for="item in visibleList"
          :key="item.id"
          class="list-item"
          :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }"
        >
          {{ item.id }}
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
const startIndex = ref(0); //可视区渲染数组的开始下标
const endIndex = ref(0); //可视区渲染数组的结束下标
const listData = ref(
  new Array(1000).fill({}).map((item, index) => ({ id: index }))
); //生成1000条要渲染的数据
const itemHeight = ref(50); //确定每一项的高度
const container = ref(null); //内容区的ref
const offsetHeight = ref(0); //偏移量
const placeholderHeight = ref(itemHeight.value * listData.value.length); //占位元素的高度
const clientHeight = ref(0); //内容区的高度
const buffer = 10;
const topIndex = ref(0);
const visibleList = computed(() => {
  return listData.value.slice(
    startIndex.value,
    Math.min(endIndex.value, listData.value.length)
  );
}); //计算属性得到要渲染的数组
const visibleCount = computed(() => {
  return Math.ceil(container.value.clientHeight / itemHeight.value);
}); //计算属性 获取当前可见区域能渲染的元素的个数
const handleScroll = (e) => {
  // 滚动变化后重新结算新的开始和结束下标
  console.log("scrollTop", e.target.scrollTop);
  topIndex.value = Math.floor(e.target.scrollTop / itemHeight.value);
  startIndex.value = Math.max(0, topIndex.value - buffer);
  endIndex.value = startIndex.value + visibleCount.value + buffer;
  offsetHeight.value = topIndex.value * itemHeight.value;
};
onMounted(() => {
  // 初始化值
  startIndex.value = 0;
  endIndex.value = startIndex.value + visibleCount.value + buffer;
  clientHeight.value = container.value.clientHeight;
});
</script>
<style lang="scss" scoped>
.virtual-container {
  position: relative;
  overflow-y: scroll;
  width: 300px;
  height: 500px;
  background-color: #baf;
  margin: 50px auto 0;
  .list-content {
    width: 100%;
    position: absolute;
    left: 0;
    top: 0;
    background-color: rgb(61, 190, 223);
    .list-item {
      box-sizing: border-box;
      border: 1px solid #ddd;
      text-align: center;
      width: 100%;
    }
  }
}
</style>

topIndex是在可视区见到的可视区顶部的下标,实际的startIndex是topIndex-buffer得到的

注意添加缓冲区后offsetHeight偏移量要用topIndex计算而不是之前的startIndex

相关推荐
AI成长日志2 小时前
【Agentic RL】1.1 什么是Agentic RL:从传统RL到智能体学习
人工智能·学习·算法
xiaotao1312 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉2 小时前
Electron桌面应用聊天(续)
前端·javascript·electron
彧翎Pro3 小时前
基于 RO1 noetic 配置 robosense Helios 32(速腾) & xsense mti 300
前端·jvm
_李小白3 小时前
【OSG学习笔记】Day 38: TextureVisitor(纹理访问器)
android·笔记·学习
小码哥_常3 小时前
解锁系统设置新姿势:Activity嵌入全解析
前端
之歆3 小时前
前端存储方案对比:Cookie-Session-LocalStorage-IndexedDB
前端
哟哟耶耶3 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐3 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
杨云龙UP4 小时前
从0到1快速学会Linux操作系统(基础),这一篇就够了!
linux·运维·服务器·学习·ubuntu·centos·ssh