如何实现虚拟列表?定高和不定高两种场景

之前我写了一篇文章:如何使用 IntersectionObserver API 来实现数据的懒加载 在文章的最后,我们提到如果加载的列表数据越来越多,我们不可能把所有的数据都渲染出来,因为这样会导致页面卡住甚至崩溃。

为了优化这种长列表场景,我们可以使用虚拟列表,核心思想是:仅渲染可视区域内(及其附近)的列表项。即不管列表有多少条数据,只取指定数量的项渲染,比如说 15 条,15 条足以覆盖可视区域以及其上下附近区域,当然这个数量视具体情况决定。

定高场景

定高的意思是我们提前知道每个列表项的高度,比如 100px。
假设我们现在有 10000 条数据,当用户上下滚动的时候,始终取对应的 15 条数据渲染。代码以 vue3 来示例。

思路分析

  • 定义 DOM 结构和滚动事件
    在这里插入图片描述
    在上述代码中,我们定义三层 DOM 结构,最外层是视口 viewport,次外层是所有列表项的父容器(这里设置其高度为整个列表内容的高度:style=“{ height: ${contentHeight}px }”),最里层是通过 v-for 循环生成的列表项。然后我们在视口层监听 scroll 事件。

  • 设置样式
    在这里插入图片描述
    这里视口的高度设置为 800px,列表项 content-item 的高度设置为 100px,且定位设为绝对定位,这很关键,因为上下滚动的时候每个列表项距离顶部的距离需要通过 top 这个定位属性来设置。

  • 渲染数据的截取
    我们整个列表 items 有 10000 条数据,当用户上下滚动的时候,我们需要知道当前位置需要截取列表中的哪 15 条数据?所以我们需要知道截取开始的位置 startIndexstartIndex 是动态变化的,它需要根据用户的滚动位置来计算。

    scrollTop 属性可以获取滚动条距离内容顶部的距离 scrollTop,scrollTop 是由 content-item 的高度撑起来的,那么 startIndex = scrollTop/100px(content-item) ,那么我们当前需要渲染的数据就是:renderItems = items.slice(startIndex, startIndex+15)

    具体代码如下:
    在这里插入图片描述
    在上述代码中,我们在使用 Array.form 方法生成列表数据的时候,每个 item 都设置了 top 属性,top = i * ITEM_HEIGHT,即第一个 item 距离顶部的距离为 0,第二个 item 距离顶部的距离为 100px,第三个 item 距离顶部的距离为 200px……,在 DOM 上设置 style::style=“{ top : ${item.top}px}” 可保证每个 item 处于正确的位置。

运行代码,上下滚动,可得到如下表现:

虚拟列表定高

在视频中我们可以看到,无论我们怎么滑动列表,最终都只会渲染 15 个列表项。

完整代码如下:

<template><div ref="viewportRef"class="viewport"@scroll="handleScroll"><div class="content-wrap":style="{ height: `${contentHeight}px` }"><div v-for="item in renderItems":key="item.id":style="{ top : `${item.top}px`}"class="content-item"><p>{{ item.text }}</p></div></div></div>
</template><script setup>
import { computed, ref } from 'vue';const ITEM_HEIGHT = 100; // 假设每个 item 的高度为 100px
const RENDER_SIZE = 15; // 假设每次渲染 15 条数据
// 假设 10000 条数据
const items = Array.from({ length: 10000 }, (v, i) => {return {id: i,text: `item-${i+1}`,top: i * ITEM_HEIGHT};
});const startIndex = ref(0);
const viewportRef = ref(null);const renderItems = computed(() => {const endIndex = startIndex.value + RENDER_SIZE;return items.slice(startIndex.value, endIndex);
});
// 整个列表的高度
const contentHeight = computed(() => {return items.length * ITEM_HEIGHT;
});const handleScroll = () => {const scrollTop = viewportRef.value?.scrollTop;// 更新 startIndexstartIndex.value = Math.floor(scrollTop / ITEM_HEIGHT);
}
</script><style>
.viewport {height: 800px;overflow-y: auto;
}
.content-wrap {position: relative;display: flex;flex-direction: column;justify-content: space-between;
}
.content-item {height: 100px; /* 假设每个项目高度为100px */position: absolute;width: 100%;border: 1px solid #000;display: flex;justify-content: center;align-items: center;
}
</style>

不定高场景

不定高是我们不能提前知道每个列表项的高度,它的高度是动态变化的,具体多高由它的数据的多少决定,有些项数据比较多,那么它最终渲染出来的 DOM 高度就比较高。这种情况就需要我们动态去计算每个列表项的高度。

思路分析

  • 定义 DOM 结构和滚动事件
    在这里插入图片描述
    这里最外层视口层的结构和之前定高的情况是一样的,次外层有两个 DOM:一个是 content-placeholder ,用于占位,它的高度就是整个列表渲染后的高度。另一个是 content-wrap ,是当前渲染列表的父容器,注意它的样式::style=“{ transform: translateY(${offset}px) }”,offset 的值是动态计算的,为的是确保在滚动的过程中,当前渲染的列表处于整个列表中正确的位置。

    最里层是当前渲染的列表,注意,我们这里的 ref 使用了函数 :ref=“(el) => renderItemsRef(el, item.id)”renderItemsRef 函数中把已渲染的列表项真实的高度存下来,用于后续的计算。

  • 设置样式
    在这里插入图片描述
    占位元素是绝对定位的

  • 渲染数据的截取
    不定高的情况下要确认 startIndex 要比定高的情况复杂得多。
    同定高的情况一下,我同样需要 scrollTop 来用于 startIndex 的计算,观察下图:
    在这里插入图片描述
    观察上图得知,当前视口渲染的第一个列表项 curList1 在整个列表中的位置即为 startIndex,我们可以根据滚动条的位置(scrollTop)来计算,也就是说,我们需要确定在 curList1 之前有几个列表项,我们从索引 0 开始遍历整个列表,把每个列表项的高度相加,当 totalHeight >= scrollTop 的时候,我们就遍历到了 curList1 这个列表项,那么当前的索引 index 就是我们需要的 startIndex,具体代码如下:
    在这里插入图片描述
    上述代码中,allItems 是整个列表数据,具体如下:
    在这里插入图片描述
    allItems 这里我们设置了一个随机高度,模拟不定高的情况,但在实际场景中,height 应该设置为一个接近于 item 渲染后的真实高度,即这里的 ITEM_HEIGHT

    同时 hasRenderedItemsHeight 就是已经渲染的列表项的高度的集合,它是一个对象,key 是列表项,value 是列表项渲染后的高度,具体的赋值代码如下,也就是我们前面提到的 renderItemsRef 函数:
    在这里插入图片描述
    在上述代码中,每次有新的列表项渲染完成,我们都需要调用 updateRenderTotalHeight 函数去更新整个列表的实际高度。

    最后,我们在组件挂载的时候和滚动事件触发的时候调用 updateRenderItems 即可实现不定高的虚拟列表
    在这里插入图片描述

    运行代码,可得到如下表现:

虚拟列表不定高

完整代码如下:

<template><div ref="viewportRef"class="viewport"@scroll="handleScroll"><div class="content-placeholder" :style="{ height: `${renderTotalHeight}px` }"></div><div class="content-wrap":style="{ transform: `translateY(${offset}px)` }"><div v-for="item in renderItems":ref="(el) => renderItemsRef(el, item.id)":key="item.id":style="{ height: `${item.height}px`}"class="content-item">{{ item.text }}</div></div></div>
</template><script setup>
import { ref, onMounted, nextTick } from 'vue';const RENDER_SIZE = 15; // 假设每次渲染 15 条数据
const ITEM_HEIGHT = 100; // 假设每个 item 真实渲染后高度接近 100px
// 假设 10000 条数据
const allItems = Array.from({ length: 10000 }, (v, i) => {return {id: i,text: `item-${i+1}`,// 设置随机高度, 在实际项目中应该根据 item 的实际情况设置一个接近于 item 渲染后的高度height: Math.floor(Math.random() * 100) + 50};
});const viewportRef = ref(null);
const renderItems = ref([]); // 当前需要渲染的 item
const renderTotalHeight = ref(0); // 整个已渲染列表的高度
const hasRenderedItemsHeight = ref({}); // 已渲染的 item 数据 height 
const offset = ref(0);const updateRenderItems = () => {const scrollTop = viewportRef.value?.scrollTop;let startIndex = 0;let startOffset = 0;for (let i = 0; i < allItems.length; i++) {const h = hasRenderedItemsHeight.value[allItems[i].id] || ITEM_HEIGHT;startOffset += h;if (startOffset >= scrollTop) {startIndex = i;break;}}renderItems.value = allItems.slice(startIndex, startIndex + RENDER_SIZE);offset.value = startOffset - hasRenderedItemsHeight.value[allItems[startIndex].id];}const renderItemsRef = (el, id) => {if (el) {// 存放已渲染的 item 的高度hasRenderedItemsHeight.value[id] = el.offsetHeight;// 更新容器的高度nextTick(updateRenderTotalHeight);}
}const updateRenderTotalHeight = () => {renderTotalHeight.value = allItems.reduce((sum, item) => sum + (hasRenderedItemsHeight[item.id] || ITEM_HEIGHT), 0);
}const handleScroll = () => {updateRenderItems();
}onMounted(() => {updateRenderItems();
})
</script><style>
.viewport {height: 800px;overflow-y: auto;position: relative;
}
.content-placeholder {position: absolute;left: 0;top: 0;right: 0;
}.content-item {width: 100%;border: 1px solid #000;display: flex;justify-content: center;align-items: center;
}
</style>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WebPack插件实现:打包之后自动混淆加密JS文件

在WebPack中调用JShaman&#xff0c;实现对编译打包生成的JS文件混淆加密 一、插件实现 1、插件JShamanObfuscatorPlugin.js&#xff0c;代码&#xff1a; class JShamanObfuscatorPlugin { apply(compiler) { compiler.hooks.emit.tapAsync(JShamanObfuscatorPlugin, (comp…

浙江大学数据结构MOOC-课后习题-第九讲-排序3 Insertion or Heap Sort

题目汇总 浙江大学数据结构MOOC-课后习题-拼题A-代码分享-2024 题目描述 测试点 思路分析 和上一题的思路一样&#xff0c;每进行一次迭代&#xff0c;来验证当前序列是否和给定的序列相同 代码展示 #include <cstdlib> #include <iostream> #define MAXSIZE 10…

【PHP小课堂】PHP中的网络组件相关函数

PHP中的网络组件相关函数 作为一门以 WEB 开发为主战场的编程语言来说&#xff0c;PHP 即使是在目前这个大环境下&#xff0c;依然也是 WEB 领域的头号玩家。我们在网络相关的功能中也提供了许多方便好用的函数组件&#xff0c;而且它们都是不需要安装扩展就能够使用的。今天&a…

ubuntu-24.04系统静态Mac和IP配置

操作系统版本&#xff08;桌面版&#xff09;&#xff1a;ubuntu-24.04-desktop-amd64.iso 原因说明&#xff1a;因网络的IP地址和Mac是预分配的&#xff0c;所以ubuntu系统需要修改网卡的mac地址和IP才能访问&#xff0c;网络查了半天资料都没成功&#xff0c;后再界面提示&a…

网上书城|基于SprinBoot+vue的网上书城管理系统(源码+数据库+文档)

网上书城管理系统 目录 基于SprinBootvue的网上书城管理系统 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3用户后台功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介…

NGINX完全指南_实现高性能负载均衡的进阶实操指南

欢迎阅读 2024 版《NGINX 完全指南》。OReilly 已连续九年出版《NGINX 完全指南》&#xff0c;我们不断进行更新以跟上 NGINX 做出的诸多改进。如今&#xff0c;NGINX 是全球最受欢迎的 Web 服务器。该产品于 2004 年首次发布&#xff0c;并不断发展&#xff0c;以满足现代应用…

【进阶篇】Midjourney如何最大限度精准控图,做到收放自如?

和朋友们一起玩MJ也已经快两个月了&#xff0c;从一开始的惊喜&#xff0c;到现在的平淡&#xff0c;大家都开始思考这个到底我们应该怎么用&#xff1f;而不是仅仅是为了满足自己对MJ本身的好奇心&#xff0c;以及生出一张美图时的那种喜悦感。 目前大家最多的疑问就是&#…

matlab生成波形然后采样,FPGA写testbench读取数据

一、在matlab产生激励 fs1000; % 这个是路数 M16; % 这个是FFT的点数&#xff0c;64K L65536; % 将N写为两个整数乘积的形式&#xff0c;即N ML&#xff0c;(log2 M和log2 L都为正整数) NM*L; % 这段 MATLAB 代码是用来生成一个时间序列的&#xff0c; % 该时间序列从0开…

搜索自动补全-elasticsearch实现

1. elasticsearch准备 1.1 拼音分词器 github地址&#xff1a;https://github.com/infinilabs/analysis-pinyin/releases?page6 必须与elasticsearch的版本相同 第四步&#xff0c;重启es docker restart es1.2 定义索引库 PUT /app_info_article {"settings": …

【机器学习】Pandas中to_pickle()函数的介绍与机器学习中的应用

【机器学习】Pandas中to_pickle()函数的介绍和机器学习中的应用 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#…

【论文阅读】 YOLOv10: Real-Time End-to-End Object Detection

文章目录 AbstractIntroductionRelated WorkMethodologyConsistent Dual Assignments for NMS-free Training &#xff08;无NMS训练的一致性双重任务分配&#xff09;Holistic Efficiency-Accuracy Driven Model Design &#xff08;效率-精度驱动的整体模型设计&#xff09; …

STM32F4_HAL_LWIP_RAM接口UDP实验

目录 RAW 的 UDP 接口简介 RAW 的 UDP 实验 硬件设计 例程功能 软件设计 UDP 配置步骤 软件设计流程图 RAW 的 UDP 接口简介 UDP 协议的 RAW 的 API 功能函数&#xff0c;我们使用这些函数来完成 UDP 的数据发送和接收功能。 (1) udp_new 函数 此函数用来创建一个 UDP …

如何用俄语预定酒店,柯桥外贸俄语培训

-Привет, Алекс! Как твои дела? 你好&#xff0c;阿列克斯&#xff01;你最近好吗&#xff1f; -Отлично! Я скоро уезжаю на 10 дней в Санкт-Петербург, но никак не могу найти там…

如何查询Windows 10电脑的IP地址

如何查询Windows 10电脑的IP地址 引言 在Windows 10操作系统中&#xff0c;查询电脑的IP地址是一项基本而重要的任务&#xff0c;无论是为了配置网络、解决连接问题&#xff0c;还是进行远程访问。 基础知识 IP地址&#xff1a;互联网协议地址&#xff0c;用于标识网络中的…

帆软报表点击表格给数据集传递参数案例

一、效果 有四个模块&#xff0c;分别是采购总金额&#xff0c;采购总数量&#xff0c;采购合同数量&#xff0c;采购合同申请数量通过点击单元格上的月份&#xff0c;展示不同的月份数据&#xff0c;进行单元格和表格之间的联动 二、准备好数据库表和展示数据内容 2.1 建表 …

私人健身与教练预约|基于SprinBoot+vue的私人健身与教练预约管理系统(源码+数据库+文档)

私人健身与教练预约管理系统 目录 基于SprinBootvue的私人健身与教练预约管理系统 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2后台功能模块 5.2.1管理员功能 5.2.2用户功能 5.2.3教练功能 四、数据库设计 五、核心代码 六、论文参考 七、最新计算…

CentOS8搭载正反向解析dns服务器

1.介绍&#xff08;是什么&#xff09; DNS&#xff08;Domain Name System&#xff09;&#xff0c;即域名系统&#xff0c;是一个将域名和 IP 地址相互映射的分布式数据库&#xff0c;它可以将用户输入的域名转换成对应的 IP 地址。DNS 由多个服务器组成&#xff0c;分为多个…

Creo模型按一定的比例放大或缩小(实际尺寸)

原来&#xff0c;距离是100mm 缩放操作 放大3倍&#xff0c;距离变为300mm

使用QtCreator C++编写串口调试助手

100编程书屋_孔夫子旧书网 1.首先看一下我设计的界面&#xff08;我这里比较简单&#xff0c;大家可根据自己的需求进行设计&#xff09; &#xff08;界面设计的过程中&#xff0c;每一个控件的名称最好进行修改&#xff0c;便于后续控件太多不好区分&#xff0c;给控件命名的…

6.1 Go 数组

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…