定高与不定高虚拟列表

前言

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

定高虚拟列表

        定高虚拟列表就是虚拟列表的每一行的高度都是固定的,所以做起来也比较方便,一个可视的容器,里面包括这一个用于撑开让可视区域出现滚动条的元素,加列表元素,通过滚动的距离除于每一行的高,得到开始坐标 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中的数据,渲染出正确的内容

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/904815.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于全景图像拼接算法

图像拼接简介图像拼接的主要步骤摄像机运动的投影模型&#xff08; projective model)图像的对齐 (registration)图像的合成 (blending)图像拼接试验 什么是图像拼接&#xff1f; 将多幅在不同时刻、从不同视角或者由不同传感器获得的图像经过对齐然后无缝地融合在一起&#xf…

二十二、MySQL 8.0 主从复制原理分析与实战

文章目录 一、复制&#xff08;Replication&#xff09;1、什么是复制2、复制的方式3、复制的数据同步类型3.1、异步复制3.2、半同步复制3.3、设计理念&#xff1a;复制状态机——几乎所有的分布式存储都是这么复制数据的 4、基于binlog位点同步的主从复制原理4.1、异步复制示例…

MFC工控项目实例二十七添加产品参数

承接专栏《MFC工控项目实例二十六创建数据库》 在型号参数界面添加三个参数试验时间、最小值、最大值。变量为double m_edit_time; double m_edit_min; double m_edit_max; 1、在SEAL_PRESSURE.h中添加代码 class CProductPara { public:union{struct{...double m_edit_min;…

java:入门基础(1)

练习一&#xff1a;文字版格斗游戏 需求: ​ 格斗游戏&#xff0c;每个游戏角色的姓名&#xff0c;血量&#xff0c;都不相同&#xff0c;在选定人物的时候&#xff08;new对象的时候&#xff09;&#xff0c;这些信息就应该被确定下来。 举例&#xff1a; ​ 程序运行之后…

Spring Boot Configuration和AutoConfiguration加载逻辑和加载顺序调整

在spring中&#xff0c; AutoConfiguration也是一个种Configuration&#xff0c;只是AutoConfiguration是不能使用proxy的。 而且spring对于两者的加载顺序也不是一视同仁&#xff0c;是有顺序的。spring会先加载SpringBootApplication可达的且标注了Configuration的类&#x…

第二十三章 Vue组件通信之非父子组件通信

目录 一、引言 1.1. event bus 事件总线 1.1.1. 实现步骤 1.2. provide & inject 1.2.1. 实现步骤 二、event bus事件总线完整代码 2.1. 工程结构图 ​2.2. main.js 2.3. App.vue 2.4. EventBus.js 2.5. BaseC.vue 2.6. BaseB.vue 2.7. BaseA.vue 三、provi…

AI代币是什么?AI与Web3结合的未来方向在哪里?

近两年随着人工智能的崛起&#xff0c;AI已经渗透到制造业、电商、广告、医药等各个行业&#xff0c;加密货币领域也不例外&#xff0c;人工智能与区块链的融合&#xff0c;让我们看到了独特的数字资产 — AI加密代币。 它的流行始于2022年底&#xff0c;随着OpenAI智能聊天机…

java_封装

基本介绍 面向对象编程有三大特征&#xff1a;封装、继承和多态。 封装介绍 封装的理解和好处 封装的实现步骤&#xff08;三步&#xff09; 案例 package com.hspedu.encap;public class Encapsulation01 {public static void main(String[] args) {// 如果要使用快捷键altr…

爬虫+数据保存2

爬取数据保存到MySQL数据库 这篇文章, 我们来讲解如何将我们爬虫爬取到的数据, 进行保存, 而且是把数据保存到MySQL数据库的方式去保存。 目录 1.使用pymysql连接数据库并执行插入数据sql代码(insert) 2.优化pymysql数据库连接以及插入功能代码 3.爬取双色球网站的数据并保…

物理模拟:OpenVDB数据与游戏引擎的结合使用

目录 OpenVDB简介 VDB&#xff08;Voxel Data Base&#xff09;存储结构 距离场&#xff08;SDF&#xff09;和密度场&#xff08;Density&#xff09; VDB格式特点 VDB应用案例 1. 网格运算 2.Ray Marching算法优化 3.模型转流体 PBRT V4 OpenVDB在Unreal Engine中的…

Java8中CompletableFuture.allOf的使用

目录标题 CompletableFuture.allOf(...)&#xff1b;CompletableFuture.allOf(...).get();CompletableFuture.allOf(...).join();总结如何优雅的处理异常呢&#xff1f; CompletableFuture.allOf(…)&#xff1b; CompletableFuture.allOf(…) 本身不会等待所有的 Completable…

VBA语言専攻介绍20241031

VBA语言専攻简介 在当今世界&#xff0c;几乎没有任何工作是没有计算机的。有些工作需要定期重复相同的过程&#xff0c;最好将它们自动化。一旦任务自动化&#xff0c;只需单击一个按钮即可运行。VBA是实现自动化工作的最为简单的方式&#xff0c;它不需要其他工具&#xff0…

OpenCV视觉分析之目标跟踪(6)轻量级目标跟踪器类TrackerNano的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 Nano 跟踪器是一个超轻量级的基于深度神经网络&#xff08;DNN&#xff09;的通用目标跟踪器。 由于特殊的模型结构&#xff0c;Nano 跟踪器速度…

Python小游戏17——飞机大战

运行结果 首先&#xff0c;你需要安装Pygame库。如果你还没有安装它&#xff0c;可以使用以下命令来安装&#xff1a; bash pip install pygame 代码&#xff1a; python import pygame import random # 初始化Pygame pygame.init() # 屏幕大小 SCREEN_WIDTH 800 SCREEN_HEIGH…

Java 基本语法与语言环境(1/30)

目录 Java 基本语法与语言环境 1. Java 语言环境搭建 1.1 安装 JDK 1.2 配置系统变量 配置步骤&#xff08;Windows 系统&#xff09; 2. Hello World 程序 2.1 代码解析 2.2 编译与运行 3. Java 程序结构 4. 基本语法规则 4.1 标识符与关键字 4.2 数据类型与变量 …

开源一个开发的聊天应用与AI开发框架,集成 ChatGPT,支持私有部署的源码

大家好&#xff0c;我是一颗甜苞谷&#xff0c;今天分享一个开发的聊天应用与AI开发框架&#xff0c;集成 ChatGPT&#xff0c;支持私有部署的源码。 介绍 当前系统集成了ChatGPT的聊天应用&#xff0c;不仅提供了基本的即时通讯功能&#xff0c;还引入了先进的AI技术&#x…

LSTM——长短期记忆神经网络

目录 1.LSTM 工作原理 2.LSTM的代码实现 3.代码详解 LSTM&#xff08;Long Short-Term Memory&#xff09;是一种特殊的循环神经网络&#xff08;RNN&#xff09;&#xff0c;用于解决长序列中的长期依赖问题。它通过引入门机制&#xff0c;控制信息的流入、保留和输出&…

大数据新视界 -- 大数据大厂之优化大数据计算框架 Tez 的实践指南

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

dedecms手机搜索不跳转手机页面模板的解决方法

1.找到文件plus/search.php&#xff0c;添加如下代码并保存 $mobile (isset($mobile) && is_numeric($mobile)) ? $mobile : 0; if ( $mobile1 ) {define(DEDEMOB, Y); } 2.来到网站后台&#xff0c;默认模板管理&#xff0c;新建模板 将手机端列表页面的.html文件&…

UE5之5.4 第一人称示例代码阅读2 子弹发射逻辑

TP_WeaponComponent.h 看看头文件 暴露了attach weapon和fire给蓝图 这两个函数意义一看名字吧&#xff0c;就是捡起来枪的时候执行&#xff0c;一个就是发射子弹的时候执行 #pragma once#include "CoreMinimal.h" #include "Components/SkeletalMeshComponen…