用原生JS实现虚拟列表(IT枫斗者)
用原生JS实现虚拟列表
介绍
- 最近在开发需求的时候,有用到 Antd 的虚拟列表组件 rc-virtual-list ,粗略地看了一下源码,于是萌生了自己写一个虚拟列表的想法。
- 当一个列表需要渲染大量数据的时候是非常耗时的,而且在列表滚动的过程中会出现卡顿的现象。即使用上懒加载解决了列表初始化时渲染过慢的问题,但是每次拉取下一页数据的时候都会造成列表的重新渲染。随着拉取的数据越来越多,列表渲染时间长、卡顿的问题依然存在。
- 这个时候虚拟列表就派上用场了。虚拟列表的实现原理简单来说,就是列表并不会把所有的数据都渲染出来,而是通过监听滚动事件然后实时计算当前是哪几条数据显示在页面上,然后只渲染用户可以看见的这几条数据。
- 在网上找了张图可以说明的更生动些:
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vbz6hBM5-1684195418798)(C:\Users\quyanliang\AppData\Roaming\Typora\typora-user-images\1684195219996.png)]
定高虚拟列表
-
实现虚拟列表需要两层的div:
-
<style>.list-container {overflow: auto;border: 1px solid black;height: 500px;} </style><body><!-- 外部容器用来固定列表容器的高度,同时生成滚动条 --><div class="list-container"><!-- 内部容器用来装元素,高度是所有元素高度的和 --><div class="list-container-inner"></div></div>Ï </body>
-
外层的div用来固定高度,也就是图上展示的B区域。内层的div用来装列表元素,也就是图上展示的A区域。给外层的div设置 overflow: auto ,这样当列表元素数量超过B区域的高度时就会出现滚动条。
-
接下来,我们需要监听外层div的滚动事件,实时计算出显示在可视区域上的第一个元素的坐标 startIndex 和 最后一个元素的坐标 endIndex。假设列表每个元素的高度都是60px,那么 startIndex 就等于外层 div 顶部卷起来的长度除以列表元素的高度,而 endIndex 就等于 startIndex + 可视区域能展示的元素个数 :
-
const itemHeight = 60 const height = 500 const startIndex = Math.floor(outerContainer.scrollTop / itemHeight) const endIndex = startIndex + Math.ceil(height / itemHeight)
-
然后用 startIndex 和 endIndex 去截取数据,并渲染在内层 div 上:
-
const viewData = data.slice(startIndex, endIndex + 1) const innerContainer = document.querySelector('.list-container-inner') innerContainer.innerHTML = '' for(let i = 0; i < viewData.length; i++) {const item = document.createElement('div')item.innerHTML = viewData[i]innerContainer.appendChild(item) }
-
最后一步,虽然我们只需要渲染在可视区域上的元素,但是我们还需要给内层的 div 设置 padding-top 和 padding-bottom ,替代那些不需要渲染的元素。由此保证虚拟列表是可以滚动的,而且滚动条位置和列表元素位置是相对应的。 padding-top 等于 startIndex 之前的元素高度和, padding-bottom 等于 endIndex 之后的元素高度和:
-
const paddingTop = startIndex * itemHeight const paddingBottom = (data.length - endIndex) * itemHeight innerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)
-
完整代码:
-
<style>.list-container {overflow: auto;border: 1px solid black;height: 500px;} </style><body><!-- 外部容器用来固定列表容器的高度,同时生成滚动条 --><div class="list-container"><!-- 内部容器用来装元素,高度是所有元素高度的和 --><div class="list-container-inner"></div></div><script>/** --------- 一些基本变量 -------- */const itemHeight = 60const height = 500/** --------- 生成数据 -------- */const initData = () => {const data = []for(let i = 0; i < 15; i++) {data.push({content: `内容:${i}`, height: itemHeight, color: i % 2 ? 'red' : 'yellow'})}return data}const data = initData()const contentHeight = itemHeight * data.lengthconst outerContainer = document.querySelector('.list-container')const scrollCallback = () => {// 获取当前要渲染的元素的坐标const scrollTop = Math.max(outerContainer.scrollTop, 0)const startIndex = Math.floor(scrollTop / itemHeight)const endIndex = startIndex + Math.ceil(height / itemHeight)const innerContainer = document.querySelector('.list-container-inner')// 从data取出要渲染的元素并渲染到容器中const viewData = data.slice(startIndex, endIndex + 1)innerContainer.innerHTML = ''for(let i = 0; i < viewData.length; i++) {const item = document.createElement('div')const itemData = viewData[i]item.innerHTML = itemData.contentitem.setAttribute('style', `height: ${itemData.height}px; background: ${itemData.color}`)innerContainer.appendChild(item)}// 未渲染的元素由padding-top和padding-bottom代替,保证滚动条位置正确const paddingTop = startIndex * itemHeightconst paddingBottom = (data.length - endIndex) * itemHeightinnerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)}// 首屏渲染scrollCallback()// 监听外部容器的滚动事件outerContainer.addEventListener('scroll', scrollCallback)</script> </body>
不定高虚拟列表
-
上面实现的是元素高度固定的虚拟列表,对于元素高度不固定的情况,虚拟列表实现起来会更复杂一些。
-
因为我们事先不知道元素的高度,所以在监听滚动事件的过程中,需要遍历一遍列表获取每个元素的高度,由此算出 startIndex 、 endIndex 等数据。不过在第一次渲染之前,因为元素没有渲染出来,我们拿不到元素的真实高度。所以这时候需要给元素一个最小高度 itemHeight ,通过 itemHeight 来计算 startIndex 和 endIndex,由此保证第一次渲染出来的元素能占满整个可视区域。
-
另外,当元素渲染出来以后,我们用一个字典去记录每个元素的真实高度,供下次滚动事件触发时 startIndex 和 endIndex 的计算。
-
startIndex 的算法是在遍历列表元素的过程中,逐步累加当前元素的高度得到 contentHeight ,当第一次出现 contentHeight 大于容器顶部卷起来的长度的时候,说明当前元素是列表可视区域的第一个元素,记为 startIndex。
-
endIndex 的算法是当第一次出现 contentHeight 大于 容器顶部卷起来的长度 + 容器高度 的时候,说明当前元素是列表可视区域的最后一个元素,记为 endIndex 。
-
完整代码:
-
<style>.list-container {overflow: auto;border: 1px solid black;height: 500px;} </style><body><!-- 外部容器用来固定列表容器的高度,同时生成滚动条 --><div class="list-container"><!-- 内部容器用来装元素,高度是所有元素高度的和 --><div class="list-container-inner"></div></div><script>/** --------- 一些基本变量 -------- */const itemHeight = 60const height = 500/** --------- 生成数据 -------- */const getRandomHeight = () => {// 返回 [60, 150] 之间的随机数return Math.floor(Math.random() * (150 - itemHeight + 1) + itemHeight)}const initData = () => {const data = []for(let i = 0; i < 15; i++) {data.push({content: `内容:${i}`, height: getRandomHeight(), color: i % 2 ? 'red' : 'yellow'})}return data}const data = initData()const cacheHeightMap = {}const outerContainer = document.querySelector('.list-container')const scrollCallback = () => {let contentHeight = 0let paddingTop = 0let upperHeight = 0let startIndexlet endIndexconst innerContainer = document.querySelector('.list-container-inner')const scrollTop = Math.max(outerContainer.scrollTop, 0)// 遍历所有的元素,获取当前元素的高度、列表总高度、startIndex、endIndexfor(let i = 0; i < data.length; i++) {// 初始化的时候因为元素还没有渲染,无法获取元素的高度// 所以用元素的最小高度itemHeight来进行计算,保证渲染的元素个数能占满列表const cacheHeight = cacheHeightMap[i]const usedHeight = cacheHeight === undefined ? itemHeight : cacheHeightcontentHeight += usedHeightif (contentHeight >= scrollTop && startIndex === undefined) {startIndex = ipaddingTop = contentHeight - usedHeight}if (contentHeight > scrollTop + height && endIndex === undefined) {endIndex = iupperHeight = contentHeight}}// 应对列表所有元素没有占满整个容器的情况if (endIndex === undefined) {endIndex = data.length - 1upperHeight = contentHeight}// 未渲染的元素的高度由padding-top和padding-bottom代替,保证滚动条位置正确// 这里如果把设置pading的操作放在渲染元素之后,部分浏览器滚动到最后一个元素时会有问题const paddingBottom = contentHeight - upperHeightinnerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)// 从data取出要渲染的元素并渲染到容器中const viewData = data.slice(startIndex, endIndex + 1)innerContainer.innerHTML = ''const fragment = document.createDocumentFragment()for(let i = 0; i < viewData.length; i++) {const item = document.createElement('div')const itemData = viewData[i]item.innerHTML = itemData.contentitem.setAttribute('style', `height: ${itemData.height}px; background: ${itemData.color}`)fragment.appendChild(item)}innerContainer.appendChild(fragment)// 存储已经渲染出来的元素的高度,供后面使用const children = innerContainer.childrenlet flag = startIndexfor(const child of children) {cacheHeightMap[flag] = child.offsetHeightflag++}}// 首屏渲染scrollCallback()// 监听外部容器的滚动事件outerContainer.addEventListener('scroll', scrollCallback)</script> </body>
-
相关文章:

用原生JS实现虚拟列表(IT枫斗者)
用原生JS实现虚拟列表 介绍 最近在开发需求的时候,有用到 Antd 的虚拟列表组件 rc-virtual-list ,粗略地看了一下源码,于是萌生了自己写一个虚拟列表的想法。当一个列表需要渲染大量数据的时候是非常耗时的,而且在列表滚动的过程…...

FAT NTFS Ext3文件系统有什么区别
10 年前 FAT 文件系统还是常见的格式,而现在 Windows 上主要是 NTFS,Linux 上主要是Ext3、Ext4 文件系统。关于这块知识,一般资料只会从支持的磁盘大小、数据保护、文件名等各种维度帮你比较,但是最本质的内容却被一笔带过。它们最…...
信息收集思路
1、开发者注释 在网站前端代码中遗留的开发者注释 其中可能包含某些关键信息 💡 使用F12 、CtrlU 、view-source: 查看前端源码 3、Robots文件 爬虫协议,网站根目录存在的robots.txt文件,用于告知搜索引擎或爬虫哪些路径和页面不…...

Tauri应用开发(二):创建第一个Tauri应用
创建tauri应用 推荐参考官方文档:https://tauri.app/v1/guides/ 创建命令: npm create tauri-applatest💡注意:请确保Node.js和Rust已经正确安装 在创建过程中,需要根据提示选择配置项。 主要配置有: 项目…...

自信裸辞:一晃 ,失业都3个月了.....
最近,找了很多软测行业的朋友聊天、吃饭 ,了解了一些很意外的现状 。 我一直觉得他们技术非常不错,也走的测开/管理的路径;二三月份裸辞的,然后一直在找工作,现在还没找到工作 。 经过我的分析࿰…...
Python3 输入和输出
在Python 3中,你可以使用内置的函数来进行输入和输出操作。 输入(Input): 要从用户那里获取输入,可以使用input()函数。input()函数会等待用户输入,并返回一个字符串。你可以将输入存储在一个变量中&#…...

Mybatis Plus 使用@TableLogic实现逻辑删除
文章目录 步骤1:修改数据库表添加deleted列步骤2:实体类添加属性步骤3:运行删除方法知识点1:TableLogic 接下来要讲解是删除中比较重要的一个操作,逻辑删除,先来分析下问题: 这是一个员工和其所签的合同表,关系是一个员工可以签多…...

2023/5/23总结
super关键字 super关键字的用法和this 关键字的用法相似 this:代表本类对象的引用(this关键字指向调用该方法的对象一般我们是在当前类中使用this关键字,所以我们常说this代表本类对象的引用)super:代表父类存储空间的标识(可以理解为父类对象…...

Squid代理服务器应用
在web架构中,用户一般进入负载均衡层,通过调度来访问web应用层,但是如果访问量太大,并发量较高,web应用层会吃不消,我们把静态资源、经常要访问的资源放入缓存,用户直接访问缓存层,加…...
网络编程中的sockfd是什么?
2023年5月22日,周一早上: 今天早上学习网络编程时遇到了sockfd这个变量,于是学习了一下,顺便写篇博客来记录自己的学习成功。 sockfd是什么意思? "sock"是socket的缩写。"fd"则是file descripto…...

如何利用Citespace和vosviewer既快又好地写出高质量的论文及快速锁定热点和重点文献进行可视化分析?
基于Citespace和vosviewer文献计量学可视化SCI论文高效写作方法 CiteSpace是什么? 简单来说,它一款通过将国内外文献进行可视化分析来帮助你了解一门学科前世今生的软件。 面对成千上万篇的文献,怎样才能快速锁定自己最感兴趣的主题及科学…...

(学习日记)AD学习 #1
写在前面: 由于时间的不足与学习的碎片化,写博客变得有些奢侈。 但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。 既然如此 不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录&a…...

缓存存在的问题
文章目录 缓存问题缓存穿透引入解决方案 缓存雪崩缓存击穿 缓存问题 使用缓存时常见的问题主要分为三个:缓存穿透 、缓存雪崩、缓存击穿。 下面对其进行一一学习 缓存穿透 引入 定义:缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#…...

ChatGPT 的 AskYourPDF 插件所需链接如何获取?
一、背景 目前 ChatGPT 主要有两款 PDF 对话插件,一个是 AskYourPDF 一个是 ChatWithPDF(需 ChatGPT Plus),他们都可以实现给一个公共的PDF 链接,然后进行持续对话,对读论文,阅读 PDF 格式的文…...

基于自营配送模式的车辆路径规划设计与实现_kaic
摘要 近年来,随着我国消费水平逐渐提升,消费者在网上购物的频率也越来越高,电商发展速度迅猛,加大了物流配送的压力,促使物流企业以更大的运力,更短的时间将货物送达。在货品的运输过程中,成本居…...

动态规划-树形DP
树的重心 题目 链接:https://www.acwing.com/problem/content/848/ 给定一颗树,树中包含 n n n 个结点(编号 1 ∼ n 1 \sim n 1∼n)和 n − 1 n-1 n−1 条无向边。 请你找到树的重心,并输出将重心删除后&#x…...

多线程基础(二)CAS无锁优化/自旋锁/乐观锁、ABA问题
CAS (Compare And Set)比较并替换 上篇文章的锁问题解决,可以使用更高效的方法,使用AtomXXX类,AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原于性的。 import java.util.ArrayList; imp…...

记ABAC的落地实践
为什么使用ABAC 一般提到授权,我们就会想到角色(role)。什么样的用户拥有什么样的角色可以怎么操作什么样的资源,这是我们普遍使用的权限系统的模型。这里的角色实质上是包含了一组用户操作资源的规则集合。一旦角色被创建&#…...

【C++】C++11线程库 和 C++IO流
春风若有怜花意,可否许我再少年。 文章目录 一、C11线程库1.thread类介绍2.mutex互斥锁 和 CAS原子操作(compare and set)3.lock_guard和unique_lock4.两个线程交替打印,一个打印奇数,一个打印偶数(线程同步…...

cpp11实现线程池(六)——线程池任务返回值类型Result实现
介绍 提交任务函数submitTask中返回的Result类型应该是用Result类包装当前的task,因为出函数之后task即如下形式:return Result(task); Result和Task都要互相持有对方的指针,Task要将任务执行结果通过Result::setVal(run()) 调用传给其对应…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...