虚拟列表的实现

news2025/1/16 5:45:39

一、什么是虚拟列表

在传统的列表渲染中,如果列表数据过多,一次性渲染所有数据将耗费大量的时间和内存。当我们上下滚动时,性能低的浏览器或电脑都会感觉到非常的卡,这对用户的体验时是致命的。

于是我们会想到懒加载,当资源到达可视窗口内时,继续向服务器发送请求获取接下来的资源,不过当获取的资源越来越多时,此时浏览器不断重绘与重排,这样的开销也是要考虑的当数量多到一定程度时,页面也会出现卡顿。

此时我们会想到虚拟列表,虚拟列表只渲染当前可见的部分数据,随着滚动条的滚动,只渲染当前可见的列表项,从而大大减少了渲染时间。同时支持无限滚动,用户只需要不停地滚动页面,就可以看到所有的数据,从而提高了用户的体验。

另外,需要注意的是,懒加载和虚拟列表也是有区别的
实现方式不同。懒加载是将页面上的图片、视频等资源延迟加载,只有当用户将它们滚动到可视区域时才会加载,从而减少页面的加载时间。虚拟列表是将大型列表数据分段加载,只渲染当前可见的部分数据,随着滚动条的滚动,只渲染当前可见的列表项,从而大大减少了渲染时间。
优化的点不同。懒加载主要是针对页面上的资源进行优化,减少页面的加载时间,这对网络繁忙卡顿有帮助。虚拟列表主要是针对大型列表数据进行优化,减少渲染时间和内存占用。
使用场景不同。懒加载适用于需要加载大量图片、视频等资源的页面,如图片展示、视频播放等。虚拟列表适用于需要渲染大量数据的页面,如电商网站中的商品列表、社交网站中的好友列表等。

二、虚拟列表的实现

在这里插入图片描述
我们用数组保存要渲染的数据,同时监听鼠标滚动事件,获取滚动的距离,通过相关计算我们计算出滚动距离下对应的数组对应的渲染的开始索引和结束索引,用slice方法截取出来得到新数组,把新数组渲染到页面。以下是具体实现

三、代码讲解

(1) html和css

强调一下第三个div的作用(.list-view-shadow),高度设置为装下所有列表的总高度,触发滚动条的出现;ref="content"这个div就是渲染区域;其他的不多于讲解

<template>
  <div class="father">
    <div class="list-view" ref="scrollBox" @scroll="handleScroll">
      <div
        class="list-view-shadow"
        :style="{
          height: contentHeight,
        }"
      ></div>
      <div ref="content" class="list-view-content">
        <div
          class="list-view-item"
          :style="{
            height: item.height + 'px',
          }"
          v-for="item in state.visibleData"
          :key="item.val"
        >
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>
 
 <style scoped>
.father {
  width: 500px;
}
.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  border: 1px solid #aaa;
}
 
.list-view-shadow {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
 
.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}
.list-view-content div:nth-child(2n + 1) {
  background-color: rgb(222, 85, 108);
}
.list-view-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  box-sizing: border-box;
  background-color: rgb(61, 217, 234);
  border: 1px solid snow;
}
 
</style>

(2)核心代码

(1)数据初始化,data用来存储传进来的数据,这里我们自己使用随机函数生成高度数据,数组computedData是辅助数组,里面存放到i元素(包括自身)的累加高度,后面滚动后参考比较滚动距离和累计高度(后面有),从而算出索引,再计算content(dom元素)该translate的距离

let scrollBox = ref(null).value;//获取dom元素
let content = ref(null).value;
let contentHeight = computed({
  get() {
    return computedData[computedData.length - 1].bom + "px";
  },
});
 
onMounted(() => {
  updateVisibleData();//首次渲染数据
});
 
let data = [];//存储随机高度,想要固定高度列表的童鞋把随机函数去掉即可
let minHeight = 35;//设置最小高度
for (let i = 0; i < 100; i++) {
  //这里val相当于列表id啦
  data.push({ val: i, height: minHeight + Math.random() * 20 });
}
 
let computedData = [];//辅助数组存储累加高度,用来修正高度,计算translate距离
for (let i = 0; i < data.length; i++) {
  let bom = data[i].height + (i === 0 ? 0 : computedData[i - 1].bom);
  computedData[i] = { val: data[i].val, bom };
}
 
 
//visibleData用来存储展示的数据
//start用来记录该渲染的数据的首个索引
let state = reactive({
  visibleData: [],
  start:0
});

(2)实现更新渲染数组的函数(重点)。首先根据木桶效应计算出渲染列表容量,各位想想如果不用minHeight会出现什么效果(大家可以shishi);这个state.start是个响应式数据,后面滚动发生高度变化,二分法计算出首个索引,state.start变化时会触发这个更新函数(下面有);然后从原数组中截取需要渲染的数组,页面绑定了这个数组,所以视图会更新;

接下来计算滚动距离,累计到当前索引高度 - 当前索引所占高度,这就实现了ref为content这个div不偏移过头,确保里面列表计算出来的首个索引的元素可以进入视图中

function updateVisibleData() {
  //计算最大容量(渲染呈现的区域 / 最小高度)
  const visibleCount = Math.ceil(scrollBox.clientHeight / minHeight);
  const end = state.start + visibleCount;
  state.visibleData = data.slice(state.start, end);
  //计算滚动距离(累计到当前索引高度 - 当前索引所占高度,这就实现了视图包含当前索引的那项div)
  let scrollLen = computedData[state.start].bom - data[state.start].height;
  //设置偏移距离(因为鼠标一直在滚动,不设置的话这个呈现列表就在原地不动啦)
  content.style.webkitTransform = `translate3d(0, ${scrollLen}px, 0)`;
}

(3)监听鼠标滚动事件,触发findStart函数,在findStart中,使用二分法寻找computedData[i].bom最先大于scrollTop的索引下标,为什么要是判断其底部是否大于scrollTop呢?因为如果滚动距离没有超过一个小列表的底部(累加)距离时(下图简称bom),我们这个列表应该还是可见的,不然页面不连贯 ,如下图
在这里插入图片描述

//用watch来监听start是否发生变化,如果发生变化才调用处理函数,这个可以提升性能
watch(()=>state.start,(newval,oldval)=>{
  if(newval!==oldval){
    updateVisibleData();
  }
})
 
function handleScroll() {
  const scrollTop = scrollBox.scrollTop;//获取滚动距离
  state.start = findStart(scrollTop);
  
}
 
function findStart(scrollTop) {
  let left = 0;
  let right = computedData.length;
  //二分法寻找start
  while (left + 1 != right) {
    let mid = Math.floor((left + right) / 2);
    if (computedData[mid].bom > scrollTop) {
      right = mid;
    } else {
      left = mid;
    }
  }
  //为什么要返回right,因为我们1判断的是bom属性,按照上面的算法来说,right下标下面的bom属性永远大于滚动距离----
  //left下标对于的bom永远小于等于滚动距离,也就是说left索引对应的元素可以不用渲染;如果滚动距离没超过某个索引下的bom属性时,当前索引有效(也就是right)。
  return left === 0 ? 0 : right;
}

另外为了减少触发updateVisibleData函数的触发,需要设置watch监听器,当state.start也就是渲染数据的索引改变时,我们才调用updateVisibleData函数。

四、全部代码

<template>
  <div class="father">
    <div class="list-view" ref="scrollBox" @scroll="handleScroll">
      <div
        class="list-view-shadow"
        :style="{
          height: contentHeight,
        }"
      ></div>
      <div ref="content" class="list-view-content">
        <div
          class="list-view-item"
          :style="{
            height: item.height + 'px',
          }"
          v-for="item in state.visibleData"
          :key="item.val"
        >
          {{ item.val }}--height--{{ item.height }}
        </div>
      </div>
    </div>
  </div>
</template>
    
<script setup>
import { onMounted, computed, ref, reactive, watch } from "vue";
let scrollBox = ref(null).value; //获取dom元素
let content = ref(null).value;
let contentHeight = computed({
  get() {
    return computedData[computedData.length - 1].bom + "px";
  },
});
 
onMounted(() => {
  updateVisibleData(); //首次渲染数据
});
 
let data = []; //存储随机高度,想要固定高度列表的童鞋把随机函数去掉即可
let minHeight = 35; //设置最小高度
for (let i = 0; i < 100; i++) {
  //这里val相当于列表id啦
  data.push({ val: i, height: minHeight + Math.random() * 20 });
}
 
let computedData = []; //辅助数组存储累加高度,用来修正高度
for (let i = 0; i < data.length; i++) {
  let bom = data[i].height + (i === 0 ? 0 : computedData[i - 1].bom);
  computedData[i] = { val: data[i].val, bom };
}
 
//visibleData用来存储展示的数据
//start用来记录该渲染的数据的首个索引
let state = reactive({
  visibleData: [],
  start: 0,
});
 
//用watch来监听start是否发生变化,如果发生变化才调用处理函数,这个可以提升性能
watch(
  () => state.start,
  (newval, oldval) => {
    if (newval !== oldval) {
      updateVisibleData();
    }
  }
);
 
function updateVisibleData() {
  //计算最大容量(渲染呈现的区域 / 最小高度)
  const visibleCount = Math.ceil(scrollBox.clientHeight / minHeight);
  const end = state.start + visibleCount;
  state.visibleData = data.slice(state.start, end);
  //计算滚动距离(累计到当前索引高度 - 当前索引所占高度,这就实现了视图包含当前索引的那项div)
  let scrollLen = computedData[state.start].bom - data[state.start].height;
  //设置偏移距离(因为鼠标一直在滚动,不设置的话这个呈现列表就在原地不动啦)
  content.style.webkitTransform = `translate3d(0, ${scrollLen}px, 0)`;
}
 
function handleScroll() {
  const scrollTop = scrollBox.scrollTop; //获取滚动距离
  state.start = findStart(scrollTop);
}
 
function findStart(scrollTop) {
  let left = 0;
  let right = computedData.length;
  //二分法寻找start
  while (left + 1 != right && right > 0) {
    let mid = Math.floor((left + right) / 2);
    if (computedData[mid].bom > scrollTop) {
      right = mid;
    } else {
      left = mid;
    }
  }
    //为什么要返回right,因为我们1判断的是bom属性,按照上面的算法来说,right下标下面的bom属性永远大于滚动距离----
  //left下标对于的bom永远小于等于滚动距离,也就是说left索引对应的元素可以不用渲染;如果滚动距离没超过某个索引下的bom属性时,当前索引有效(也就是right)。
  return left === 0 ? 0 : right;
}
</script>
    <style scoped>
.father {
  width: 500px;
}
.list-view {
  height: 400px;
  overflow: auto;
  position: relative;
  border: 1px solid #aaa;
}
 
.list-view-shadow {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
  visibility: hidden;
}
 
.list-view-content {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}
.list-view-content div:nth-child(2n + 1) {
  background-color: rgb(222, 85, 108);
}
.list-view-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #666;
  box-sizing: border-box;
  background-color: rgb(61, 217, 234);
  border: 1px solid snow;
}
</style>

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

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

相关文章

如何使用c3p0连接池???

1.首先下载架包。。。&#xff08;下载链接&#xff1a;https://note.youdao.com/ynoteshare/index.html?id61e2cc939390acc9c7e5017907e98044&typenote&_time1693296531722&#xff09; 2.将架包加入项目文件。 创建一个lib目录&#xff0c;将架包复制进去 右键点击l…

ManageEngine ServiceDesk Plus之CVE漏洞

什么是CVE&#xff1f; CVE的英文全称是“Common Vulnerabilities & Exposures”即通用漏洞披露&#xff0c;CVE像是一个字典表&#xff0c;为广泛认同的信息安全漏洞给出一个公共的名称。 使用一个公共名称&#xff0c;可以帮助用户在各自独立的各种漏洞数据库中共享数据…

价格管控的有效措施

品牌为了发展&#xff0c;会不断拓展销售渠道&#xff0c;随之也会有更多的经销商加入&#xff0c;经销商变多了&#xff0c;渠道问题也会逐渐突显&#xff0c;比如低价、窜货、假货&#xff0c;窜货和假货问题其实就是低价问题&#xff0c;所以治理好价格&#xff0c;就是解决…

数据倾斜优化

数据倾斜发生的原因有哪些&#xff1f; map输出数据按key Hash的分配到reduce中&#xff0c;由于key分布不均匀、业务数据本身的特性、建表时考虑不周等原因造成的reduce 上的数据量差异过大。 数据倾斜解决方式有哪些 group by 导致的数据倾斜 1.开启Map-Side聚合后&#x…

MSLearn 开学季:AI 进阶系列|PromptFlow - 做一个教育行业的 Copilot 应用

点击蓝字 关注我们 编辑&#xff1a;Alan Wang 排版&#xff1a;Rani Sun 微软 Reactor 为帮助广开发者&#xff0c;技术爱好者&#xff0c;更好的学习 .NET Core, C#, Python&#xff0c;数据科学&#xff0c;机器学习&#xff0c;AI&#xff0c;区块链, IoT 等技术&#xff0…

美创科技一体化智能化公共数据平台数据安全建设实践

公共数据是当今政府数字化转型的关键要素和未来价值释放的核心锚点&#xff0c;也是“网络强国”、“数字中国”的战略性资源。 作为数字化改革先行省份&#xff0c;近年来&#xff0c;浙江省以一体化智能化公共数据平台作为数字化改革的支撑总平台&#xff0c;实现了全省公共数…

华为云云服务器评测|华为云云耀云服务器L实例评测使用

文章目录 华为云云耀云服务器L实例简介华为云云耀云服务器L实例参数华为云云耀云服务器L实例部署网站的步骤华为云云耀云服务器L实例的核心卖点华为云云耀云服务器L实例适用于多种核心应用场景华为云云耀云服务器L实例总体感受 华为云云耀云服务器L实例简介 华为云云耀云服务器…

解决方案 | 法大大电子签:智慧银行建设背后的“助推器”

随着互联网技术的不断发展&#xff0c;银行业正在经历一场前所未有的数字化革命&#xff0c;平台搭建、场景化金融、在线金融逐渐成为商业银行转型智慧银行的发力点。而电子合同在银行业的广泛应用&#xff0c;为银行业务流程的无纸化、高效能提供了强有力的支持。 聚焦效率及…

高频电流探头的钳口使用和如何调零消磁

高频电流探头采用霍尔效应传感器技术来测量交流和直流信号&#xff0c;标配通用的 BNC 接口&#xff0c;可直接用示波器或记录仪等观察测量波形及数值&#xff0c;具有强大的通用性能。 电流探头钳口使用&#xff1a; 为电流指示方向。测量时&#xff0c;被测导体电流方向与指…

SSM - Springboot - MyBatis-Plus 全栈体系(一)

第一章 Maven 高效构建Java应用&#xff1a;Maven入门和进阶 一、Maven 简介和快速入门 1. Maven 介绍 Maven 是一款为 Java 项目构建管理、依赖管理的工具&#xff08;软件&#xff09;&#xff0c;使用 Maven 可以自动化构建、测试、打包和发布项目&#xff0c;大大提高了…

java自动登录 selenium 自动登录并获取cookie

选择操作网页 我用的edge&#xff0c;谷歌我的版本太高没有对应的驱动… 下载Edge的驱动程序&#xff0c;直接解压就好里面只有一个.exe文件 https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ 复制即用&#xff0c;看注释 import com.alibaba.fastjs…

【TI毫米波雷达笔记】毫米波雷达芯片out_of_box开箱demo代码解读(以IWR6843AOP为例)

【TI毫米波雷达笔记】毫米波雷达芯片out_of_box开箱demo代码解读 结构框架&#xff1a; blog.csdn.net/weixin_53403301/article/details/132522364功能 IWR6843AOP的开箱工程是根据IWR6843AOPEVM开发板来的 该工程可以将IWR6843AOP的两个串口利用起来 实现的功能主要是两个…

开学季,“护眼教室”上线,守护孩子光明未来

在关于教室的记忆里&#xff0c;你是否有着这样的滤镜&#xff1f; “电影”滤镜&#xff1a;模糊又遥远&#xff0c;夜间自习像褪色的胶片。 “眩光”滤镜&#xff1a;灯光亮到出现“光环”&#xff0c;看不了多久眼睛就酸胀。 “频闪”滤镜&#xff1a;头顶的灯仿佛飞速眨眼…

Android Mvvm设计模式的详解与实现

一、介绍 在开发设计模式中&#xff0c;模式经历了多次迭代&#xff0c;从MVC到MVP&#xff0c;再到如今的MVVM。发现的过程其实很简单&#xff0c;就是为了项目更好的管理。 设计模式严格来说属于软件工程的范畴&#xff0c;但是如今在各大面试中或者开发中&#xff0c;设计模…

MySQL 更新语句是怎么执行的

更新语句的执行流程 更新语句的执行流程图为什么要两阶段提交呢&#xff1f; 更新语句的执行流程图 更新语句的执行是 Server 层和引擎层配合完成&#xff0c;数据除了要写入表中&#xff0c;还要记录相应的日志。 update 语句的执行流程图&#xff0c;图中浅色框表示是在 In…

为何反射探针关闭Mipmap后变成了白图

1&#xff09;为何反射探针关闭Mipmap后变成了白图 2&#xff09;2021.3 Android从AssetBundle中加载视频播放失败问题 3&#xff09;SBP是否可以解决打包时FBX等模型文件中额外的GameObject 4&#xff09;Addressables加载已打包过的Prefab后Mono脚本丢失 这是第349篇UWA技术知…

CSS3D+动画

CSS3D 1.css3D 给父元素设置 perspective:景深:近大远小的效果900-1200px这个范围内 transform-style:是否设置3D环境 flat 2D环境 默认值 perserve-3D环境 3D功能函数 1.位移: translateZ()translate3D(x,y,z) <!DOCTYPE html> <html lang"en"><h…

【Python小练习】利用DES进行加密解密

from Crypto.Cipher import DES from Crypto.Util.Padding import pad, unpad import json# 创建 DES 加密对象 key b123456 # 8字节的密钥&#xff0c;注意必须为字节类型 cipher DES.new(key, DES.MODE_ECB)# 加密 def encrypt_data(data):plaintext json.dumps(data).en…

安防视频监控/视频集中存储/云存储平台EasyCVR平台无法播放HLS协议该如何解决?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

【Java基础篇】一文搞懂Java方法的调用与重载(超详细)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【JavaSE_primary】 目录 一、方法的概念以及使用1.1什么是方法1.2方法定义1.3方法调用的执行过程1.4形参和实参的关系 二、方法的重载方…