JS实现高度不等的列表虚拟滚动加载

       当我们拿到后台返回的1万条数据展示时,如果完全渲染不仅渲染的时间很长,也会导致浏览器性能变差,使用过程中还可能会导致卡顿,使用体验变差。
就需要我们想办法优化这种情况,这个时候使用虚拟滚动加载就能很好的避免这种情况。

虚拟滚动的实现原理:只加载用户当前可见区域内的内容,即滚动列表时,动态改变可视区域内的渲染内容
在这里插入图片描述
根据这个原理,我们需要知道一屏加载多少条数据,设置一个默认的子元素高度,根据展示区域计算出一个可见条数,再设置一些缓存区数量,避免滚动就需要重新加载

// 设置的默认子元素高度
var minSize = 25
// 子元素默认高度 情况下可见数量条数
var viewCount = Math.ceil(dom.clientHeight / minSize)
// 缓存区数量(虚拟滚动当前屏展示量以外 多展示的条目)
var bufferSize = 6
// 默认条数+缓存区数量后的总条数
var bufferSizeViewCount = viewCount + bufferSize * 2 

因为是不等高列表,当我们跳转指定行或者做其他更多操作时,需要知道指定行的具体信息,所以缓存所有子元素的布局信息。先设置每行默认的布局信息,后续页面滚动加载时再实时更新

// 缓存位置信息
var topNum = 0
var bottomNum = 0
var height = minSize
for (let index = 0; index < tableData.length; index++) {const item = tableData[index];topNum = index == 0 ? 0 : (topNum + height)bottomNum = topNum + heightconst obj2 = {index,height,top: topNum,bottom: bottomNum,isUpdate: false}rowPosList.push(obj2);
}

根据需要加载的开始位置和结束位置,加载虚拟滚动页面的内容,默认加载从第一行开始的数据

// 当前开始下标(滚动top)
let startIndex = 0;
// 当前结束下标(根据盒子高度时可见数量条数得到结束下标)
let endIndex = Math.min(tableData.length - 1, startIndex + viewCount);
// 算上缓存区数量后,真实需要加载的起始行和结束行下标
let startBufferIndex = Math.max(0, startIndex - bufferSize);
let endBufferIndex = Math.min(tableData.length - 1, endIndex + bufferSize);
// 生成dom节点
let documentFragment = document.createDocumentFragment()
for (let i = startBufferIndex; i <= endBufferIndex; i++) {const item = tableData[i]// 当前行展示内容let htmlText = "<li class='liText' rowindex='"+item.index+"'>序号:" + item.index +",内容:"+ item.text +"</li>"// 将 HTML 字符串转换为 DOM 元素,并添加到文档片段中const tempElement = document.createElement('template');tempElement.innerHTML = htmlText;const elList = [].slice.call(tempElement.content.children)for (const el of elList) {documentFragment.appendChild(el);}
}
listInnerDom.innerHTML = ""
listInnerDom.appendChild(documentFragment)

加载成功后就需要设置滚动条,为了后面滚动加载其他的内容,我们使用padding撑开盒子的高度,而padding的值就根据缓存的rowPosList每一元素的布局信息来设置

// 设置滚动条高度(利用上下边距撑开)
const startOffset = rowPosList[startBufferIndex].top;
const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;
listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);

滚动加载就意味着需要根据滚动的高度获取需要加载内容,我们使用滚动top值从缓存信息rowPosList的top、bottom值来进行判断获取当前滚动距离对应的行下标

// 二分查询优化
function findItemIndex(scrollTop) {let low = 0;let high = rowPosList.length - 1;while (low <= high) {const mid = Math.floor((low + high) / 2);const { top, bottom } = rowPosList[mid];if (scrollTop >= top && scrollTop <= bottom) {high = mid;break;} else if (scrollTop > bottom) {low = mid + 1;} else if (scrollTop < top) {high = mid - 1;}}return high;
}

虚拟滚动实际上就是移除旧的dom,插入新dom的操作,如果操作过于频繁就会影响性能,所以我们需要进行限制。因为我们加了缓存区的概念,所以理所当然的在缓存区范围内滚动就不需要再重新加载内容

// 在缓存区范围以外才进行渲染更新
if(startIndex < startBufferIndex || endIndex > endBufferIndex){
}

而为了后续的一些针对列表的操作,例如直接查看第666行的数据,就需要对这行的数据有清楚的定位信息,且确保跳转后再次上下滚动加载不会导致出现问题(白屏情况),所以需要实时更新之前的布局信息以供这种操作

// 更新每个item的位置信息 
function upCellMeasure() {const rowList = listInnerDom.querySelectorAll(".liText");if (rowList.length === 0) { return }const firstIndex = startBufferIndex;const lastIndex = endBufferIndex;// 当前渲染的数据高度是否有改动,有则需要修改当前渲染数据后面所有存储的坐标信息let hasChange = falselet dHeight = 0for (let index = 0; index < rowList.length; index++) {const rowItem = rowList[index];const rowIndex = rowItem.getAttribute("rowindex");let rowPosItem = rowPosList[rowIndex]const prevRowPosItem = rowPosList[rowIndex - 1];// 设置当前行的top|bottom|height等属性可能导致列高度的改变,已经设置过的不再重新设置if (rowPosItem && (!rowPosItem.isUpdate || (prevRowPosItem && prevRowPosItem.top + prevRowPosItem.height != rowPosItem.top))) {const rectBox = rowItem.getBoundingClientRect();const top = prevRowPosItem ? prevRowPosItem.bottom : 0;let height = rectBox.heightdHeight = dHeight + (height - rowPosItem.height)Object.assign(rowPosList[rowIndex], {height,top,bottom: top + height,isUpdate: true});hasChange = true}}// // 鼠标滑轮滚动 解决向上滚动时,前面行的坐标信息未更新,会导致后面设置padding的值导致滚动偏移// // 在直接跳转到指定行后,向上滚动,在下一篇幅重新渲染后pdfMain的scrollTop会向下偏移,所以使用之前scroll事件记录的domScrollTop进行累加计算// if(mouseInTable && firstIndex < oldFirstIndex) {//   maskDom.scrollTop= domScrollTop + dHeight;// }// 更新未渲染的listItem的top值if (hasChange) {for (let i = lastIndex + 1; i < rowPosList.length; i++) {// if(!rowPosList[i].isUpdate){const prevRowPosItem = rowPosList[i - 1];const top = prevRowPosItem ? prevRowPosItem.bottom : 0;Object.assign(rowPosList[i], {top,bottom: top + rowPosList[i].height});// }}}// console.log(rowPosList)// 设置滚动条高度(利用上下边距撑开)const startOffset = rowPosList[startBufferIndex].top;const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);oldFirstIndex = firstIndex;
}

完整代码:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>虚拟滚动加载不等高列表</title><style>*{margin: 0;padding: 0;}button {width: 100px;margin: 20px 0 0 50px;}#masks {position: relative;width: 500px;height: 750px;margin: 20px 50px;overflow: auto;border: 1px solid #666;}.ulBox{height: 100%;}.listInner li{list-style: none;border-bottom: 1px solid #ccc;padding: 5px 10px;}</style>
</head><body><button onclick="jumpRow(666)">跳转第666</button><div class="pdf-main" id="masks" onscroll="myScroll()"><div class="ulBox"><ul class="listInner"><!-- 加载数据 --></ul></div></div><script>// 随机生成1万条不同内容的数组数据var tableData = []var datalist = "神鼎飞丹砂发生过大范甘迪发生过发给的孤独果然废物感染树大根深单个重新发但是山东分公司电话该发给非固定阿斯顿发顺丰阿萨法打算神鼎飞丹砂发生过电视上地方郭发的郭旦法大范甘迪的的高度房"var length = datalist.lengthfor (let index = 0; index < 10000; index++) {var randomNumber0 = Math.floor(Math.random() * length);var randomNumber1 = Math.floor(Math.random() * length);var num1 = Math.min(randomNumber0, randomNumber1)var num2 = Math.max(randomNumber0, randomNumber1)tableData.push({ index, text: datalist.slice(num1, num2) })}// 缓存的每一行的信息(height、top、bottom等等)var rowPosList = []// 设置的默认子单元高度var minSize = 25// 子单元默认高度 情况下可见数量条数var viewCount = 0// 缓存区数量(虚拟滚动当前屏展示量以外 多展示的条目)var bufferSize = 6// 默认条数+缓存区数量后的总条数var bufferSizeViewCount = 0 var maskDom = document.getElementById("masks")// 缓存位置信息var topNum = 0var bottomNum = 0var height = minSizefor (let index = 0; index < tableData.length; index++) {const item = tableData[index];topNum = index == 0 ? 0 : (topNum + height)bottomNum = topNum + heightconst obj2 = {index,height,top: topNum,bottom: bottomNum,isUpdate: false}rowPosList.push(obj2);}viewCount = Math.ceil(maskDom.clientHeight / minSize);bufferSizeViewCount = viewCount + bufferSize * 2console.log(tableData,rowPosList)var showTableDataList = []      // 当前虚拟滚动展示的数据var domScrollTop = 0     // 滚动距离var requestId = ""  // 限制当前屏是否完全加载var startBufferIndex = -1   // 当前加载数据中第一个所在下标var endBufferIndex = -1  // 当前加载数据中最后一个所在下标var oldFirstIndex = 0  // 当前加载数据中第一个所在下标(旧值)var listInnerDom = document.querySelector(".listInner")// 更新加载的数据(初始页面加载、滚动加载、点击选中列)autoSizeVirtualList(0)// 更新加载的数据(初始页面加载、滚动加载、跳转指定行)function autoSizeVirtualList(scrollTop, jumpRow_no) {// 根据滚动的高度值 || 或者需要跳转至指定行的行下标,判断当前加载下标let startIndex = jumpRow_no != undefined ? jumpRow_no : findItemIndex(scrollTop);const endIndex = Math.min(tableData.length - 1, startIndex + viewCount);// 页面是否重新加载let dataChange = false// 判断当前 startIndex 是否在缓冲区内。在缓冲区内,不需要重新渲染if (endIndex > 0 && (startIndex < startBufferIndex || endIndex > endBufferIndex)) {startBufferIndex = Math.max(0, startIndex - bufferSize);endBufferIndex = Math.min(tableData.length - 1, endIndex + bufferSize);showTableDataList = []let documentFragment = document.createDocumentFragment()for (let i = startBufferIndex; i <= endBufferIndex; i++) {const item = tableData[i]const rectBox = rowPosList[i]showTableDataList.push({ item, rectBox })// 当前行展示内容let htmlText = "<li class='liText' rowindex='"+item.index+"'>序号:" + item.index +",内容:"+ item.text +"</li>"// 将 HTML 字符串转换为 DOM 元素,并添加到文档片段中const tempElement = document.createElement('template');tempElement.innerHTML = htmlText;const elList = [].slice.call(tempElement.content.children)for (const el of elList) {documentFragment.appendChild(el);}}showTableDataList = showTableDataListlistInnerDom.innerHTML = ""listInnerDom.appendChild(documentFragment)dataChange = true}// 重新加载后需要更新缓存的定位信息if (dataChange) {upCellMeasure();}// 跳转到指定行查看if (jumpRow_no != undefined) {maskDom.scrollTop = rowPosList[jumpRow_no].top}domScrollTop = maskDom.scrollToprequestId = null;}// 二分查询优化function findItemIndex(scrollTop) {let low = 0;let high = rowPosList.length - 1;while (low <= high) {const mid = Math.floor((low + high) / 2);const { top, bottom } = rowPosList[mid];if (scrollTop >= top && scrollTop <= bottom) {high = mid;break;} else if (scrollTop > bottom) {low = mid + 1;} else if (scrollTop < top) {high = mid - 1;}}return high;}// 更新每个item的位置信息 function upCellMeasure() {const rowList = listInnerDom.querySelectorAll(".liText");if (rowList.length === 0) { return }const firstIndex = startBufferIndex;const lastIndex = endBufferIndex;// 当前渲染的数据高度是否有改动,有则需要修改当前渲染数据后面所有存储的坐标信息let hasChange = falselet dHeight = 0for (let index = 0; index < rowList.length; index++) {const rowItem = rowList[index];const rowIndex = rowItem.getAttribute("rowindex");let rowPosItem = rowPosList[rowIndex]const prevRowPosItem = rowPosList[rowIndex - 1];// 设置当前行的top|bottom|height等属性可能导致列高度的改变,已经设置过的不再重新设置if (rowPosItem && (!rowPosItem.isUpdate || (prevRowPosItem && prevRowPosItem.top + prevRowPosItem.height != rowPosItem.top))) {const rectBox = rowItem.getBoundingClientRect();const top = prevRowPosItem ? prevRowPosItem.bottom : 0;let height = rectBox.heightdHeight = dHeight + (height - rowPosItem.height)Object.assign(rowPosList[rowIndex], {height,top,bottom: top + height,isUpdate: true});hasChange = true}}// // 鼠标滑轮滚动 解决向上滚动时,前面行的坐标信息未更新,会导致后面设置padding的值导致滚动偏移// // 在直接跳转到指定行后,向上滚动,在下一篇幅重新渲染后pdfMain的scrollTop会向下偏移,所以使用之前scroll事件记录的domScrollTop进行累加计算// if(mouseInTable && firstIndex < oldFirstIndex) {//   maskDom.scrollTop= domScrollTop + dHeight;// }// 更新未渲染的listItem的top值if (hasChange) {for (let i = lastIndex + 1; i < rowPosList.length; i++) {// if(!rowPosList[i].isUpdate){const prevRowPosItem = rowPosList[i - 1];const top = prevRowPosItem ? prevRowPosItem.bottom : 0;Object.assign(rowPosList[i], {top,bottom: top + rowPosList[i].height});// }}}// console.log(rowPosList)// 设置滚动条高度(利用上下边距撑开)const startOffset = rowPosList[startBufferIndex].top;const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);oldFirstIndex = firstIndex;}// 滚动加载数据function myScroll() {if (requestId) return;requestId = requestAnimationFrame(() => {let scrollTop = maskDom.scrollToplet lastItem = tableData[tableData.length - 1]let lastShowItem = showTableDataList[showTableDataList.length - 1].item// 判断向下滚动到最后一篇幅时,就不再更新数据(即滚到底了就不更新)if (lastItem.index == lastShowItem.index && scrollTop >= domScrollTop) {requestId = null;return}domScrollTop = scrollTop// 更新加载的数据(初始页面加载、滚动加载、点击选中列)autoSizeVirtualList(scrollTop)});}// 跳转到指定行function jumpRow(jumpRow_no){autoSizeVirtualList(undefined,jumpRow_no)}</script>
</body></html>

效果如下:

在这里插入图片描述

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

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

相关文章

ESXi服务器无法安装Windows11:“不符合此版本的Windows所需最低系统要求“

目录 一、问题描述1.使用环境2.问题截图3.问题解析 二、解决方法Ⅰ1.按 ShiftF10 弹出命令提示符2.在弹出的Dos框中输入regedit&#xff0c;回车&#xff0c;进入注册表。3.打开HKEY_LOCAL_MACHINE\SYSTEM\Setup&#xff0c;并新建 LabConfig 的项&#xff0c;在 LabConfig 下创…

SAP 查询中间表

可以看到如下代码中&#xff0c;查询了底表zdbconn&#xff0c;又查了中间表ZTFI0072 DATA: gv_dbs(20) ,go_exc_ref TYPE REF TO cx_sy_native_sql_error,gv_error_text TYPE string,lv_count TYPE syst_index.SELECT SINGLE conntxtFROM zdbconn INTO gv_dbsWHERE sy…

RK3568 Android 11 蓝牙BluetoothA2dpSink 获取用于生成频谱的PCM

Android 中的 A2DP Sink A2DP Sink 在 Android 系统中主要用于 接收 其他蓝牙设备&#xff08;如手机、平板、电脑等&#xff09;发送过来的 高质量的立体声音频。简单来说&#xff0c;它让你的 Android 设备可以充当一个 蓝牙音箱 或 耳机 的角色。 核心功能&#xff1a; 接…

【Java】SpringBoot 单体项目创建 与 整合 Mybatis-Plus

文章目录 前言1. 创建项目与整合MP1.1 IDEA创建SpringBoot项目1.2 SpringBoot整合Mybatis-Plus 2. 远程仓库2.1 创建远程仓库/本地仓库2.2 Add/Commit/Push/Pull 3. 总结与补充3.1 解决refusing to merge unrelated histories3.2 总结3.3 结语 参考资料 SpringBoot 单体项目创建…

Hadoop环境搭建

一、Linux环境准备 Linux命令查询https://www.linuxcool.com/ http://linux.51yip.com/ 安装Linux虚拟机 安装 sudo apt install open-vm-tools 安装 sudo apt install open-vm-tools-desktop &#xff08;可选&#xff09;换国内源 ​​ sudo apt update 更新软件列表&…

火焰传感器详解(STM32)

目录 一、介绍 二、传感器原理 1.原理图 2.引脚描述 三、程序设计 main.c文件 IR.h文件 IR.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 火焰传感器是一种常用于检测火焰或特定波长&#xff08;760nm-1100nm&#xff09;红外光的传感器。探测角度60左右&am…

Docker占用根目录/存储空间过多如何清理?

问题背景 使用df -h查看磁盘空间时发现根目录空间不多了&#xff0c;已使用96%&#xff0c;红色警告&#xff01;&#xff01;&#xff01; 于是使用df -h /* 一层一层定位&#xff0c;终于找到了一个大文件 9G多的文件夹&#xff0c;位置是&#xff1a; /var/lib/docker/o…

无线通信-WIFI通信

文章目录 1. 基础知识2. 工作模式3. AT指令4. 常用AT指令实例5. 连接原子云6. 使用usb转ttl模块测试ATK-MW8266D7. 使用STM32F103ZET6战舰开发板透传模式8. 使用STM32F103ZET6战舰板连接原子云 1. 基础知识 ATK-ESP-01 ATK-ESP-01模块支持标准的IEEE802.11b/g/n协议&#xff0c…

【Linux】文件魔法师:时间与日历的解密

欢迎来到 CILMY23 的博客 &#x1f3c6;本篇主题为&#xff1a;文件魔法师&#xff1a;时间与日历的解密 &#x1f3c6;个人主页&#xff1a;CILMY23-CSDN博客 &#x1f3c6;系列专栏&#xff1a;Python | C | C语言 | 数据结构与算法 | 贪心算法 | Linux | 算法专题 | 代码…

【uniapp重大bug】uni-data-select的localdata改变,也会触发@change方法

bug描述 uni-data-select的下拉列表值localdata是动态获取的&#xff0c;且绑定了change方法&#xff0c;在页面加载后&#xff0c;请求localdata的列表数据&#xff0c;给localdata重新赋值&#xff0c;此时发现自动触发了change方法 当前uni版本&#xff1a;^2.0.2-30709202…

Axure RP10安装教程(Pro版)

下载链接 https://ga90eobypbb.feishu.cn/docx/UyzSd4q8SoXySjxtrcac4QnVn3f Axure RP 是一款专业的快速原型设计工具。它能帮助用户高效地创建网页和移动应用的线框图、流程图、原型和规格说明文档。拥有丰富的交互组件&#xff0c;可模拟各种复杂交互效果&#xff0c;如点击…

【千帆AppBuilder】使用Python调用基于官方的API创建图片故事的应用,一起体验下全代码模式下是怎样的效果

欢迎来到《小5讲堂》 这是《千帆》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 背景基本信息名称简介角色指令 能力扩展组件对话开场白推荐问 模型选…

98.SAP MII功能详解(12)Workbench-Transaction Logic(For Next Loop)

目录 1.Logic->For Next Loop 2.演示 配置对象 配置连接 for循环的整体演示 1.Logic->For Next Loop 此操作用于在预定义的次数内执行任务。每次迭代都会执行直接跟随For Next循环操作的所有操作&#xff0c;直到达到To限制。 若要在达到To属性限制之前停止&…

旅行追踪和行程规划工具AdventureLog

什么是 AdventureLog &#xff1f; AdventureLog 是一种记录您的旅行并与世界分享的简单方法。您可以在日志中添加照片、笔记等。跟踪您访问过的国家、探索去过的地区和地方。您还可以查看您的旅行统计数据和里程碑。AdventureLog 旨在成为您终极的旅行伴侣&#xff0c;帮助您记…

【LLM】文生视频相关开源数据集(VidGen、Panda、Cogvideox等)

note 总结了VidGen数据集、Panda-70m数据集、Openvid数据集、OpenVid-1M数据集、Cogvideox训练数据准备过程、ShareGPT4Video数据集等在一篇综述中还总结了评估指标包括&#xff1a;峰值信噪比&#xff08;PSNR&#xff09;、结构相似性指数&#xff08;SSIM&#xff09;、Inc…

matlab 将数组从左向右翻转

目录 一、概述1、算法概述2、主要函数二、代码示例1、翻转行向量2、翻转字符元胞数组3、翻转多维数组三、参考链接本文由CSDN点云侠翻译,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的抄袭狗。 一、概述 1、算法概述 将数组从左向右翻转 2、主要…

C++实现彩虹猫时空隧道特效(无害)

#include <Windows.h> // 如果不是在Visual Studio环境下运行的话W最好改小写。 using namespace std;int main() {for (int i 1; i < 10; i) {HDC hdc GetWindowDC(GetDesktopWindow());RECT rect;GetWindowRect(GetDesktopWindow(), &rect);StretchBlt(hdc, r…

[工具使用]ellisys

工具打开&#xff1a; 1.连接ellisys电源&#xff0c;ellisys Computer接口USB连接电脑&#xff0c;Logic接口与板子出信号的GPIO口连接 工具配置 1.点击"Configure" 2.在打开的Recording options中选择Wireless选项卡 2.选择Wired选项卡​ i.勾选Logic transit…

中秋佳节,悦动之选,精选热门骨传导耳机深度推荐

在这个金秋送爽、月圆人团圆的中秋佳节&#xff0c;我们不仅仅沉浸在月饼的香甜与家人的温馨之中&#xff0c;更渴望一份能够连接心与自然的独特礼物&#xff0c;让这份团聚的时光更加丰富多彩。在这个充满诗意的季节里&#xff0c;我满怀欣喜地向您推荐一款集科技、健康与时尚…

Maven的相关配置和使用

Maven的配置&#xff1a; Maven的配置和Java差不多&#xff0c;从镜像站下载相关的Maven版本压缩包&#xff0c;然后解压到自己的D盘&#xff0c;在进行系统变量的配置&#xff0c;新建变量Maven_HOME&#xff0c;然后值设置为Maven的地址&#xff0c;一定是点开文件后就能出现…