虚拟列表【vue】等高虚拟列表/非等高虚拟列表

文章目录

  • 1、等高虚拟列表
  • 2、非等高虚拟列表

1、等高虚拟列表

参考文章1
参考文章2
在这里插入图片描述
在这里插入图片描述

<!-- eslint-disable vue/multi-word-component-names -->
<template><divclass="waterfall-wrapper"ref="waterfallWrapperRef"@scroll="handleScroll"><div :style="scrollStyle"><div v-for="item in computedData.items" :key="item.userId">{{ item.username }}</div></div></div>
</template><script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { sameHighData } from "../data/index";const itemHeight = 18; //每个item的高度
let itemCount = ref(500); // 设置总共长度为500
const scrollTop = ref(0); // 滚动的高度
const wrapperHeight = ref(0); // 滚动容器的高度
const waterfallWrapperRef = ref();const list = ref([...sameHighData]);const handleScroll = (e: any) => {// 在content内容中距离content盒子的上部分的滚动距离scrollTop.value = e.target.scrollTop;// 判断是否接近底部,如果是则加载更多数据if (e.target.scrollHeight - e.target.clientHeight - scrollTop.value < 100) {getData();}
};const getData = () => {list.value = list.value.concat(list.value);itemCount.value = list.value.length;// scrollBarRef.value.style.height = itemCount.value * itemHeight + "px";console.log("发送网络请求", list.value.length);
};const scrollStyle = computed(() => ({height: `${itemHeight * (list.value.length - computedData.value.startIndex)}px`,transform: `translateY(${itemHeight * computedData.value.startIndex}px)`,
}));// 这里不使用方法的原因是以来了scrollTop 响应式数据来变更 所以使用computed更方便
const computedData = computed(() => {// 可视区域的索引const startIndex = Math.floor(scrollTop.value / itemHeight);// 设置上缓冲区为2 上缓冲区的索引const finialStartIndex = Math.max(0, startIndex - 2);// 可视区显示的数量 这里设置了item的高度为50const numVisible = Math.floor(wrapperHeight.value / itemHeight);// 设置下缓冲区的结束索引 上缓冲区也设置为2const endIndex = Math.min(itemCount.value, startIndex + numVisible + 2);// 将上缓冲区到下缓冲区的数据返回const items: any = [];// contentWrapRef.value!.style.height = finialStartIndex * itemHeight + "px";for (let i = finialStartIndex; i < endIndex; i++) {const item = {...list.value[i],top: itemHeight * i + "px",transform: `translateY(${itemHeight * i + "px"})`,};items.push(item);}console.log(startIndex);return { items, startIndex };
});onMounted(() => {// 在页面一挂载就获取滚动区域的高度if (waterfallWrapperRef.value) {wrapperHeight.value = waterfallWrapperRef.value?.clientHeight;}
});
</script><style scoped>
.waterfall-wrapper {height: 500px;overflow: hidden;overflow-y: scroll;position: relative;.scroll-bar {width: 100%;position: absolute;}
}
</style>

2、非等高虚拟列表

参考链接

非等高序列列表的实现逻辑并没有通过相对定位的方式,而是通过设置translateY来实现滚动
思路:

  • 难点1:每个元素的高度不一样,没办法直接计算出容器的高度
    • 容器高度的作用是足够大 可以让用户进行滚动,所以我们可以直接假设每一个元素的高度 需要保证预测的 item 高度尽量比真实的每一项 item 的高度要小或者接近所有 item 高度的平均值
  • 难点2: 每个元素高度不一样,top值不能通过count * index算出来
  • 难点3: 每个元素高度不一样,不能通过scrollTop * size计算出已经滚动的元素个数,很难获取可视区的起始索引
    • 难度2和难度3的解决方案是先把数据渲染到页面上 之后再通过 获取正式dom的高度 来计算
      在这里插入图片描述
<template><divclass="waterfall-wrapper"ref="waterfallWrapperRef"@scroll="handleScroll"><!-- 内容显示的区域 --><div ref="listRef" :style="scrollStyle"><div v-for="(item, index) in computedData" :key="index">{{ index }} {{ item.sentence }}</div></div></div>
</template>
  • 1、先确定预测的item高度和数据
  • 2、获取滚动区域的高度且计算容器最大容量
onMounted(async () => {// 在页面一挂载就获取滚动区域的高度if (waterfallWrapperRef.value) {wrapperHeight.value = waterfallWrapperRef.value?.clientHeight;// 预测容器最多显示多少个元素maxCount.value = Math.ceil(wrapperHeight.value / estimatedHeight) + 1;await nextTick();// 先把数据显示再页面上initData();setItemHeight();}
});
  • 3、引用一个新的变量来存放实际的dom和预测结果和实际的偏差高度
interface IPosInfo {// 当前pos对应的元素索引index: number;// 元素顶部所处位置top: number;// 元素底部所处位置bottom: number;// 元素高度height: number;// 自身对比高度差:判断是否需要更新dHeight: number;
}

在这里插入图片描述

  • 4、初始化预测的dom

参考逻辑

function initPosition() {const pos: IPosInfo[] = [];//  将获取到的数据全部进行初始化for(let i = 0; i < props.dataSource.length; i++) {pos.push({index: item.id,height: props.estimatedHeight, // 使用预测高度先填充 positionstop: item.id * props.estimatedHeight,bottom: (item.id + 1) * props.estimatedHeight,dHeight: 0,})}positions.value = pos;
}

实际代码

// 初始化数据
const initData = () => {const items: IPosInfo[] = [];// 获取需要更新位置的dom长度 即新增加的数据const len = list.value.length - preLen.value;// 已经处理好位置的长度const currentLen = initList.value.length;// 可以用三元运算符是因为初始化的时候initList的数值是空的const preTop = initList.value[currentLen - 1]? initList.value[currentLen - 1].top: 0;const preBottom = initList.value[currentLen - 1]? initList.value[currentLen - 1].bottom: 0;for (let i = 0; i < len; i++) {const currentIndex = preLen.value + i;// 获取当前要初始化的itemconst item: DiffHigh = list.value[currentIndex];items.push({id: item.id,height: estimatedHeight, // 刚开始不知道他高度 所以暂时先设置为预置的高度top: preTop? preTop + i * estimatedHeight: currentIndex * estimatedHeight,// 元素底部所处位置 这里获取的是底部的位置 所以需要+1bottom: preBottom? preBottom + (i + 1) * estimatedHeight: (currentIndex + 1) * estimatedHeight,// 高度差:判断是否需要更新dHeight: 0,});}initList.value = [...initList.value, ...items];preLen.value = list.value.length;
};
  • 5、更新实际的数据,拿到实际的数据高度
    • 通过 ref 获取到 list DOM,进而获取到它的 children
    • 遍历 children,针对于每一个 DOM item 获取其高度信息,通过其 id 属性找到 positions 中对应的 item,更新该 item 信息
// 数据渲染之后更新item的真实高度
const setItemHeight = () => {const nodes = listRef.value.children;if (!nodes.length) return;//  Array.from(nodes): 使用 Array.from() 方法,其中 nodes 是一个类数组对象。// [...nodes]: 使用数组解构语法,其中 nodes 是一个可迭代对象,例如 NodeList。// 1、遍历节点并获取位置信息// 2、更新节点高度和位置信息:// 这里只更新了视图上的bottom 没有更新top的数值 而且只更新视图上面显示的,并没有更新整个列表,因为height发生了改变视图以外的数据也发生了变化 需要同步修改[...nodes].forEach((node: Element, index: number) => {const rect = node.getBoundingClientRect();const item = initList.value[startIndex.value + index];const dHeight = item?.height - rect?.height;if (dHeight) {item.height = rect.height;item.bottom = item.bottom - dHeight;item.dHeight = dHeight;}});// 3、将当前 item 的 dHeight 进行累计,之后再重置为 0 (更新后就不再存在高度差了)const len = initList.value.length;let startHeight = initList.value[startIndex.value]?.dHeight;if (startHeight) {initList.value[startIndex.value].dHeight = 0;}// 从渲染视图的第二个开始处理// 实际上第一项的 top 值为 0,bottom 值在上轮也更新过了,所以遍历的时候我们从第二项开始for (let i = startIndex.value + 1; i < len; i++) {const item = initList.value[i];item.top = initList.value[i - 1].bottom;item.bottom = item.bottom - startHeight;if (item.dHeight !== 0) {startHeight += item.dHeight;item.dHeight = 0;}}// 设置 list 高度listHeight.value = initList.value[len - 1].bottom;
};
  • 6、设置滚动

// 已经滚动的距离
const offsetDis = computed(() =>startIndex.value > 0 ? initList.value[startIndex.value - 1]?.bottom : 0
);const scrollStyle = computed(() => ({height: `${listHeight.value - offsetDis.value}px`,transform: `translateY(${offsetDis.value}px)`,
}));

在这里插入图片描述

  • 7、滚动事件和 startIndex 计算
    • 如何判断一个 item 滚出视图?这个问题在最早就提到过了,只需要看它的 bottom <= scrollTop
    • 现在就好办了,我们可以遍历 positions 数组找到第一个 item.bottom >= scrollTop 的 item,它就是 startIndex 所对应的 item,那 startIndex 就拿到了
    • 这里再补充一个细节,在 initList 数组中 item.bottom 一定是递增的,而我们现在想要做的是查找操作,有序递增 + 查找 = 二分查找
// 二分查找 找到可视区域的索引startIndex
const getStartIndex = (list: any, value: number) => {let left = 0,right = list.length - 1,templateIndex = -1;while (left < right) {const mid = Math.floor((left + right) / 2);const midValue = list[mid].bottom;// 如果找到了就用找到的索引 + 1 作为 startIndex,因为找到的 item 是它的 bottom 与 scrollTop 相等,即该 item 已经滚出去了if (midValue === value) return mid + 1;else if (midValue < value) left = mid + 1;else if (midValue > value) {if (templateIndex == -1 || templateIndex > mid) {templateIndex = mid;}right = mid;}}return templateIndex;
};
  • 8、每次 startIndex 改变,不仅会改变 renderList 的计算,我们还需要重新计算 item 信息
watch(() => startIndex.value,() => {setItemHeight();}
);
<!-- eslint-disable vue/multi-word-component-names -->
<template><divclass="waterfall-wrapper"ref="waterfallWrapperRef"@scroll="handleScroll"><!-- 内容显示的区域 --><div ref="listRef" :style="scrollStyle"><div v-for="(item, index) in computedData" :key="index">{{ index }} {{ item.sentence }}</div></div></div>
</template><script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { sameDiffData } from "../data/index";interface IPosInfo {// 当前pos对应的元素索引id: number;// 元素顶部所处位置top: number;// 元素底部所处位置bottom: number;// 元素高度height: number;// 高度差:判断是否需要更新dHeight: number;
}interface DiffHigh {id: number;sentence: string;
}const waterfallWrapperRef = ref();
const listRef = ref();const estimatedHeight = 60; // 设置预估的高度为100
let itemCount = ref(500); // 设置总共长度为500
const scrollTop = ref(0); // 滚动的高度
const wrapperHeight = ref(0); // 滚动容器的高度
const preLen = ref(0); // 因为每次处理数值的时候都需要处理全部数据 这个用来记录已经处理的数据const list = ref<DiffHigh[]>([...sameDiffData]);
const initList = ref<IPosInfo[]>([]); // 用来存放已经处理好位置的数据
const listHeight = ref(0); // 列表的高度const startIndex = ref(0);
const maxCount = ref(0);const endIndex = computed(() =>Math.min(list.value.length, startIndex.value + maxCount.value + 2)
);const computedData = computed(() =>list.value.slice(Math.max(0, startIndex.value - 2), endIndex.value)
);// 已经滚动的距离
const offsetDis = computed(() =>startIndex.value > 0 ? initList.value[startIndex.value - 1]?.bottom : 0
);const scrollStyle = computed(() => ({height: `${listHeight.value - offsetDis.value}px`,transform: `translateY(${offsetDis.value}px)`,
}));const handleScroll = (e: any) => {scrollTop.value = e.target.scrollTop;// 在content内容中距离content盒子的上部分的滚动距离// 在开始滚动之后获取startIndexstartIndex.value = getStartIndex(initList.value, scrollTop.value);console.log(startIndex.value, "-0-");// 判断是否接近底部,如果是则加载更多数据if (e.target.scrollHeight - e.target.clientHeight - scrollTop.value < 100) {getMoreData();}
};watch(() => startIndex.value,() => {setItemHeight();}
);const getMoreData = async () => {list.value = list.value.concat(list.value);await nextTick();initData();setItemHeight();
};// 二分查找 找到可视区域的索引startIndex
const getStartIndex = (list: any, value: number) => {let left = 0,right = list.length - 1,templateIndex = -1;while (left < right) {const mid = Math.floor((left + right) / 2);const midValue = list[mid].bottom;if (midValue === value) return mid + 1;else if (midValue < value) left = mid + 1;else if (midValue > value) {if (templateIndex == -1 || templateIndex > mid) {templateIndex = mid;}right = mid;}}return templateIndex;
};// 初始化数据
const initData = () => {const items: IPosInfo[] = [];// 获取需要更新位置的dom长度const len = list.value.length - preLen.value;// 已经处理好位置的长度const currentLen = initList.value.length;// 可以用三元运算符是因为初始化的时候initList的数值是空的const preTop = initList.value[currentLen - 1]? initList.value[currentLen - 1].top: 0;const preBottom = initList.value[currentLen - 1]? initList.value[currentLen - 1].bottom: 0;for (let i = 0; i < len; i++) {const currentIndex = preLen.value + i;// 获取当前要初始化的itemconst item: DiffHigh = list.value[currentIndex];items.push({id: item.id,height: estimatedHeight, // 刚开始不知道他高度 所以暂时先设置为预置的高度top: preTop? preTop + i * estimatedHeight: currentIndex * estimatedHeight,// 元素底部所处位置 这里获取的是底部的位置 所以需要+1bottom: preBottom? preBottom + (i + 1) * estimatedHeight: (currentIndex + 1) * estimatedHeight,// 高度差:判断是否需要更新dHeight: 0,});}initList.value = [...initList.value, ...items];preLen.value = list.value.length;
};// 数据渲染之后更新item的真实高度
const setItemHeight = () => {const nodes = listRef.value.children;if (!nodes.length) return;//  Array.from(nodes): 使用 Array.from() 方法,其中 nodes 是一个类数组对象。// [...nodes]: 使用数组解构语法,其中 nodes 是一个可迭代对象,例如 NodeList。// 1、遍历节点并获取位置信息// 2、更新节点高度和位置信息:[...nodes].forEach((node: Element, index: number) => {const rect = node.getBoundingClientRect();const item = initList.value[startIndex.value + index];const dHeight = item?.height - rect?.height;if (dHeight) {item.height = rect.height;item.bottom = item.bottom - dHeight;item.dHeight = dHeight;}});// 3、将当前 item 的 dHeight 进行累计,之后再重置为 0 (更新后就不再存在高度差了)const len = initList.value.length;let startHeight = initList.value[startIndex.value]?.dHeight;if (startHeight) {initList.value[startIndex.value].dHeight = 0;}for (let i = startIndex.value + 1; i < len; i++) {const item = initList.value[i];item.top = initList.value[i - 1].bottom;item.bottom = item.bottom - startHeight;if (item.dHeight !== 0) {startHeight += item.dHeight;item.dHeight = 0;}}// 设置 list 高度listHeight.value = initList.value[len - 1].bottom;
};onMounted(async () => {// 在页面一挂载就获取滚动区域的高度if (waterfallWrapperRef.value) {wrapperHeight.value = waterfallWrapperRef.value?.clientHeight;// 预测容器最多显示多少个元素maxCount.value = Math.ceil(wrapperHeight.value / estimatedHeight) + 1;await nextTick();// 先把数据显示再页面上initData();setItemHeight();}
});
</script><style scoped>
.waterfall-wrapper {height: 300px;/* overflow: hidden; */overflow-y: scroll;/* position: relative; */.scroll-bar {width: 100%;}
}
</style>

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

js使用import到本js文件中的函数时报错 Error [ERR_MODULE_NOT_FOUND]: Cannot find module

node:internal/process/esm_loader:97internalBinding(errors).triggerUncaughtException(^Error [ERR_MODULE_NOT_FOUND]: Cannot find module D:\桌面\Pagesizedetection\lib\screensize imported from D:\桌面\Pagesizedetection\index.js Did you mean to import ../lib/sc…

OpenLayers多要素旋转平移缩放及olext深度定制化

目录 1.前言2.olext官方示例3.重写Transform.js4.自定义样式5.自定义选中机制6.拓展思考6.1包围框的角度问题6.2不选中要素如何平移 7总结 1.前言 首先OpenLayers本身是支持旋转、平移、缩放的。olext 只是在 OpenLayers 的基础上又做了一层封装&#xff0c;使得看起来比较好看…

【k8s核心概念与专业术语】

k8s架构 1、服务的分类 服务分类按如下图根据数据服务支撑&#xff0c;分为无状态和有状态 无状态引用如下所示&#xff0c;如果一个nginx服务&#xff0c;删除后重新部署有可以访问&#xff0c;这个属于无状态&#xff0c;不涉及到数据存储。 有状态服务&#xff0c;如redis&a…

【Pytorch深度学习开发实践学习】B站刘二大人课程笔记整理lecture06 Logistic回归

【Pytorch深度学习开发实践学习】B站刘二大人课程笔记整理lecture06 Logistic回归 课程网址 Pytorch深度学习实践 部分课件内容&#xff1a; import torchx_data torch.tensor([[1.0],[2.0],[3.0]]) y_data torch.tensor([[0.0],[0.0],[1.0]])class LogisticRegressionModel(…

Redis高性能原理

redis大家都知道拥有很高的性能&#xff0c;每秒可以支持上万个请求&#xff0c;这里探讨下它高性能的原理。单线程架构和io多路复用技术。 一&#xff0c;单线程架构 单线程架构指的是命令执行核心线程是单线程的&#xff0c;数据持久化、同步、异步删除是其他线程在跑的。re…

关于使用Mxnet GPU版本运行DeepAR报错解决方案

1.引言 我们经常使用GPU来训练和部署神经网络&#xff0c;因为与CPU相比&#xff0c;它提供了更多的计算能力。在本教程中&#xff0c;我们将介绍如何将GPU与MXNet GluonTS一起使用。 首先&#xff0c;确保您的机器中至少有一个Nvidia GPU&#xff0c;并正确安装了CUDA以及CUDN…

ES6内置对象 - Map

Map&#xff08;Map对象保存键值对&#xff0c;键值均不限制类型&#xff09; 特点&#xff1a; 有序&#xff08;Set集合是无序的&#xff09;&#xff1b;键值对&#xff08;键可以是任意类型&#xff09;&#xff1b;键名不能重复&#xff08;如果重复&#xff0c;则覆盖&…

第九节HarmonyOS 常用基础组件28-Select

1、描述 提供下拉选择菜单&#xff0c;可以让用户在多个选项之间选择。 2、接口 Select(options:Array<SelectOption>) 3、SelectOption对象说明 参数名 参数类型 必填 描述 value ResourceStr 是 下拉选项内容。 icon ResourceStr 否 下拉选项图标。 4…

c语言经典测试题3

1.题1 int a 248, b 4; int const *c 21; const int *d &a; int *const e &b; int const * const f &a; 请问下列表达式哪些会被编译器禁止&#xff1f; A: *c 32; B: *d 43 C: e&a D: f0x321f 我们来分析一下&#xff1a;const用来修饰变量是想其…

HTML5新婚、年会、各种聚会的现场抽奖活动(附源码)

文章目录 1.抽奖平台设计来源1.1 主界面效果1.2 抽奖效果1.3 中奖效果 2.效果和源码配置2.1 动态效果2.2 人员信息配置2.3 奖品信息配置2.4 抽奖音效配置2.5 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/deta…

智能运维都有哪些工作?智能运维哪些领域好

智能运维领域包含的各项工作内容包括&#xff1a; 数据采集与管理&#xff1a;该工作内容涉及从各种设备和系统中收集数据&#xff0c;如性能数据、日志数据等&#xff0c;并对这些数据进行清洗、转换和整合。数据采集与管理为后续的分析和决策提供了可靠的数据基础。 分析与诊…

函数栈帧的创建及销毁(超详解)

目录 1.预备知识 1.1内存区的划分 1.2认识相关寄存器和汇编指令 1.2.1寄存器 1.2.2相关汇编指令 2.测试前 2.1测试代码及环境 2.2 main函数也是被其他函数调用的 3.函数栈帧的创建 4.进入函数内部 5.形参与实参 6.call/jump add函数 7.函数栈帧的销毁 7.1保存…

Nginx -2

接着上文写 5.4.7 验证模块 需要输入用户名和密码 模块名称&#xff1a;ngx_http_auth_basic_module 访问控制基于模块 ngx_http_auth_basic_module 实现&#xff0c;可以通过匹配客户端资源进行限制 语法&#xff1a; Syntax: auth_basic string | off; Default: auth_ba…

【STC8A8K64D4开发板】第2-13讲:SPI总线的应用

第2-13讲&#xff1a;SPI总线的应用 学习目的了解SPI总线的结构、特点以及4种通信模式。掌握通过SPI读、写和擦除SPI Flash W25Q128的方法以及代码编写。掌握通过SPI读、写铁电存储器FM25CL64B的方法以及代码编写。 SPI总线原理 SPI是串行外设接口(Serial Peripheral Interfa…

2024-02-23(Spark)

1.RDD的数据是过程数据 RDD之间进行相互迭代计算&#xff08;Transaction的转换&#xff09;&#xff0c;当执行开启后&#xff0c;代表老RDD的消失 RDD的数据是过程数据&#xff0c;只在处理的过程中存在&#xff0c;一旦处理完成&#xff0c;就不见了。 这个特性可以最大化…

力扣随笔之按奇偶排序数组(简单905)

思路1&#xff1a;根据双指针对撞指针的思路&#xff0c;定义一个左指针从数组前端开始遍历&#xff0c;定义一个右指针从后端开始遍历&#xff0c;这时候有四种情况 左奇右偶&#xff1a;这种情况需要将其位置交换&#xff0c;将偶数提前&#xff0c;奇数后移左奇右奇&#xf…

vue 导出,下载错误提示、blob与json数据转换

一、成功/失败 - 页面展示 失败 成功 二、成功/失败 - 接口请求/响应展示成功 2. 失败 三、解决 // 导出列表exportReceivedExcel() {if (this.tableCheckedValue) {this.form.ids this.tableCheckedValue.map(v > {return v.id || null})}this.loadingReceivedExcel …

xhell链接虚拟机失败,只需检查以下三步配置

配置一个静态ip地址 然后很重要的一步&#xff1a;修改起始地址&#xff0c;范围要包括你选定机器的地址。 成功啦

文件上传漏洞--Upload-labs--Pass10--双写绕过

一、什么是双写绕过 顾名思义&#xff0c;双写绕过就是双写文件后缀名来进行绕过&#xff0c;如&#xff1a;test.php 双写后为 test.pphphp。通常情况下双写绕过用于绕过源代码中的 str_ireplace()函数。 二、双写绕过原理 1、首先进行代码审计&#xff0c;源代码中有黑名单…

【EI会议征稿通知】第十届机械工程、材料和自动化技术国际会议(MMEAT 2024)

2024年第十届机械工程、材料和自动化技术国际会议(MMEAT 2024) 2024 10th International Conference on Mechanical Engineering,Materials and Automation Technology 2024年第十届机械工程、材料和自动化技术国际会议( MMEAT 2024) 将于2024年06月21-23日在中国武汉举行。MM…