原生JS实现虚拟列表

news/2024/11/15 13:27:22/文章来源:https://www.cnblogs.com/zhangyunling/p/18368942

什么是虚拟列表

如果我们要将一个数组渲染为列表添加到页面中,我们可以很容易实现,无非就是循环遍历这个数组,然后依次创建 DOM 元素插入即可,但是如果数据量很庞大,比如有一万条数据,我们就要把一万个 DOM 结点插入到页面中,这显然会导致页面的卡顿。为了针对这个场景进行优化,让页面和我们这个列表不那么卡顿,就可以使用虚拟列表来解决。

虚拟列表说起来也很简单,由于视口高度是固定的,比如视口中只能容纳 10 个元素,那视口之外的 DOM 元素实际上是没有意义的,用户完全感知不到,我们就可以让其永远都只有 10 个元素,而不需要把所有的 DOM 元素全部插进去,可以大大优化性能,而至于这 10 个元素具体是什么,是完全可以根据滚动条的位置和每一项元素的位置来算出来的,计算这 10 个元素具体是什么这一操作是在 JS 引擎内部完成的,不涉及到 WEBAPI,更谈不上回流,所以哪怕数组里有上万项数据,我们也不必太担心性能问题。当然这里面其实涉及到很多细节,后面一一解释,这里只需要知道虚拟列表是什么即可。

虚拟列表的实现细节

接下来讨论虚拟列表具体如何实现,这里直接上图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上图

image

在这里,蓝色区域就是我们的视口,绿色区域就是总高度,这里不再将列表项全部添加到视口容器中,而是值添加可见项,这里需要注意一个细节,不能忽略滚动效果,比如我们视口只能容纳 5 项,那么到底是添加第一项到第五项,还是第五项到第十项,这是要根据滚动条的高度计算出来的。所以我们一样需要实现滚动效果,所以这里我们需要一个虚拟元素,也就是图中的绿色元素,这个绿色元素完全没有内容,绿色元素的高度实际上就是把列表项插入之后的总高度,举个例子,假设列表项定高 50 像素,列表项一共有 1000 项,那么绿色区域的高度就应该是 50000 像素,这个绿色区域的唯一用处就是用来撑开视口的内容区域实现滚动,没有别的作用。

这里也请注意,虽然看上去列表项好像是绿色区域的子元素,但实际上我在具体实现上将他们的 DOM 结构设置为平级了,这里只是用定位把他们叠在一起了,因为这个绿色区域本身就是一个虚拟的元素,他并没有实际的功能,不应该成为列表的父元素。

为了实现这个效果,我们在实现的时候应该考虑以下几个问题

  • 要渲染的第一个元素是什么
  • 要渲染的最后一个元素是什么
  • 第一个元素距离顶部的偏移量是多少
  • 虚拟元素的高度应该设置为多少

如果把这样几个问题考虑清楚了,实际上一切都很好说了,我们如果知道了第一个元素和最后一个元素的索引,那么就可以将数据中要显示的元素用 slice 切下来,然后渲染到页面中去,这里还存在一个问题,当我们将元素插入到列表中后,此时应该是插入到列表的顶部了,但是我们的滚动条有可能已经滚动了很多距离了,这个时候插入到列表顶部的话我们是看不到的,所以我们就需要给列表设置一个向下的偏移量,确保列表永远都显示在视口内部。而虚拟元素的总高度主要就是用来撑开视口的,这里我们前面已经介绍过。

我们这里分两种情况来考虑虚拟列表的具体实现,分别是定高和不定高,即数据项高度固定和数据项高度不固定。

定高的虚拟列表

定高的虚拟列表实现相对比较简单,因为我们很容易计算出虚拟元素的高度以及要渲染的第一个元素应该是什么。

首先,由于每一项高度是固定的,我们不妨设为 itemHeight,假设数据有 n 项,那么虚拟元素的总高度就应该是 itemHeight * n,这应该没有难度。

然后就是要渲染的第一项了,要渲染的第一项也很简单,假设我们目前滚动的距离是 scrollTop,那么要渲染的第一项应该就是 Math.floor(scrollTop/itemHeight)。

最后一项也并不难,由于视口高度和每一项高度都是固定的,我们直接用视口高度除以每一项高度向上取整,就得到了视口内可容纳的最大元素数量,然后加上要渲染的第一个元素下标,就计算出要渲染最后一个元素的下标。

最后要考虑的问题就是列表距离顶部的偏移量,这个偏移量不应该直接设为 scrollTop,应该是 scrollTop-(scrollTop % itemHeight),如果读者想不通为什么,可以自行使用 scrollTop 测试一下,看看会出现什么问题。

此时我们有了所有数据,要做的就很简单,无非设置一个滚动事件,当滚动的时候,根据 scrollTop 计算出要渲染的第一项下标,根据第一项下标、容器尺寸、每一项高度计算出要渲染的最后一项下标,然后根据 scrollTop 和每一项的高度计算出列表的偏移量,设置给列表即可。

对虚拟元素(前面图里面的绿色区域)高度设置应该才初始化就完成,因为对于固定高度的列表来说,列表总高度是不会改变的。

接下来给出具体的实现代码:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><link rel="stylesheet" href="./style.css">
</head><body><div class="container"><div class="phantom"></div><div class="list"></div></div>
</body>
<script src="./script.js"></script>
</html>
* {margin: 0;padding: 0;}.container {height: 300px;border: 1px solid black;width: 300px;margin: 50px auto;overflow: auto;position: relative;
}.list {height: 300px;width: 100%;position: absolute;
}.phantom {height: 100%;width: 100%;position: absolute;
}.list>div {height: 50px;width: 100%;text-align: center;box-sizing: border-box;line-height: 50px;border: 1px solid black;
}
// 初始化一个空数组,用于存储大量数据
const data = [];
for (let i = 0; i < 100000; i++) {data.push(i);
}
// 获取列表容器和幽灵元素(用于模拟数据加载效果)
const list = document.querySelector('.list');
const phantom = document.querySelector('.phantom');
// 定义每个元素的高度为50px
const itemHeight = 50;
// 定义容器的高度为300px
const containerHeight = 300;
// 获取容器元素
const container = document.querySelector('.container');
// 计算所有元素的总高度
const totalHeight = data.length * itemHeight;
// 使用幽灵元素撑开容器,以适应内容的高度
phantom.style.height = totalHeight + 'px';
// 计算容器能容纳的最大元素个数
const maxCount = Math.ceil(containerHeight / itemHeight);
/*** 渲染函数,根据滚动位置动态加载数据*/
function render() {// 清空列表内容,准备重新渲染list.innerHTML = '';// 获取容器的滚动高度const scrollTop = container.scrollTop;// 计算当前滚动位置对应的起始数据索引const start = Math.floor(scrollTop / itemHeight);// 计算当前滚动位置对应的结束数据索引const end = Math.ceil(start + maxCount);// 从数据数组中截取当前需要显示的数据片段const temp = data.slice(start, end + 1);// 计算列表顶部的偏移高度,使其与最近的数据元素对齐const offsetTop = scrollTop - scrollTop % itemHeight;// 遍历数据片段,创建并添加到列表中temp.forEach(item => {const div = document.createElement("div");div.innerText = item;list.append(div);});// 应用偏移高度,使列表内容与滚动位置对齐list.style.transform = `translateY(${offsetTop}px)`;
}
// 初始加载
render();
// 监听容器滚动事件,实时调整列表内容
container.addEventListener("scroll", render);

点我看效果

读者可以自行测试一下,哪怕这个列表项有十万项,性能也相当好,怎么拖都不会出现卡顿的情况。

不定高的虚拟列表

不定高的虚拟列表会稍微复杂一丢丢,主要也是集中在计算上,只要有了前面所说的四个东西,即

  • 要渲染的第一个元素是什么
  • 要渲染的最后一个元素是什么
  • 第一个元素距离顶部的偏移量是多少
  • 虚拟元素的高度应该设置为多少

只要计算出这四个东西,那么不定高虚拟列表也是可以很轻松实现的。

接下来考虑如何计算,由于虚拟列表每一项高度是不固定的,实际上是根据内容来动态撑开的,具体来说,如果元素内容多就会很高,如果元素内容少,就会很低。由于此时元素的高度是不确定的,所以我们有必要使用一个高度缓存列表将每一个元素的高度都缓存下来。

我们要计算的这四个东西,实际上都要用到每个元素的高度,这是绕不开的,但是这里的问题是,我们只能获取到数据,我们并不知道元素的高度是多少,或者说,对于一个 DOM 元素而言,我们无法在不将元素插入到页面的情况下拿到这个元素的高度,我们当然可以将元素插入到一个隐藏的 DOM 中,插入之后就可以获取到每一个元素的高度了,但这样显然失去了使用虚拟 DOM 的意义,这里我使用的方案是先给一个相对较小的值,也就是说将高度缓存中每一项的默认值都只给一个很小的值,可以认为这就是一个预估高度,当该元素被渲染的时候更新缓存数据即可。

由于此时虚拟列表中的元素高度不固定,在计算上面提到的四个属性的时候也会存在区别。

  • 虚拟元素的高度:也就是所有 item 的总高度,在定高的虚拟列表中,总高度可以直接根据每一项的高度和总项数相乘得到。但是在不定高的虚拟列表中这样显然是不可以的,我们需要遍历高度缓存列表,高度缓存列表的和就是总高度,这里需要注意,由于虚拟元素的高度是依赖于缓存列表高度的,所以如果缓存列表更新之后,我们也需要重新设置虚拟元素高度。
  • 第一项要渲染的元素下标:我们可以获取到 scrollTop,也可以根据缓存列表获取到每一项的高度,哪怕前面的项目可能没有高度,在缓存列表中我们依然是给他初始化了一个预估高度的,所以不用担心这一点。我们只需要遍历高度缓存列表,将高度缓存列表的数据依次累加,如果累加数值大于了 scrollTop,那么上一项就应该是我们要渲染的第一项。同时,这个累加值减去当前项的高度(这是由于这里的逻辑是先累加再判断的)就可以得到列表的偏移高度。
  • 最后一项要渲染的元素下标:我们可以继续从第一项要渲染元素的下一项开始遍历高度缓存列表,一样是从 0 累加每一项的高度,如果高度之和大于了视口高度,那么这一项就是最后一项要渲染的元素下标。

其实这里存在一个小问题,但是问题并不大,完全可以接受。

以上我们得到了我们想要的所有数据,我们只需要在滚动的时候动态获取和渲染即可。

具体实现参见以下代码:


<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style>* {margin: 0;padding: 0;}.container {width: 500px;height: 500px;border: 1px solid red;margin: 50px auto;position: relative;overflow: auto;}.container .list,.container .virtualElement {position: absolute;left: 0;top: 0;width: 100%;}.container .list>div {border: 1px solid black;}</style>
</head><body><div class="container"><div class="virtualElement"></div><div class="list"></div></div>
</body>
<script src="mock.js"></script><script>//获取DOMconst container = document.querySelector(".container");const list = document.querySelector(".list");const virtualElement = document.querySelector(".virtualElement");const containerHeight = container.clientHeight;//容器高度const forecastItemHeight = 50;//预测每一项的高度,这里最好是最小高度const data = Mock.mock({//模拟出数据"data|50-100": ["@paragraph(1,5)"]}).data.map((item, index) => index + item);const itemsHeight = Array(data.length).fill(forecastItemHeight);//保存每一项高度的缓存refreshAllheight();//刷新总高度let firstIndex;//当前从哪个元素开始渲染let offsetTop = 0;//顶部偏移量,用于设置list的下偏移let scrollTop = container.scrollTop;//当前container元素的scrollTopfunction refreshAllheight() {//根据itemsHeight数组刷新总高度const allHeight = itemsHeight.reduce((accumulate, item) => accumulate + item, 0);virtualElement.style.height = allHeight + "px";}function render() {//用于根据scrollTop进行列表渲染//清空list数据list.innerHTML = "";let accumulate = 0;//累计高度//遍历itemsHeight找到firstIndexfor (let i = 0; i < itemsHeight.length; i++) {accumulate += itemsHeight[i];if (accumulate > scrollTop) {firstIndex = i;offsetTop = accumulate - itemsHeight[i] - itemsHeight[i - 1] - itemsHeight[i - 2];//更新上偏移量,这里设置了缓存,还要减去前两项的高度break;}}//根据上偏移量设置list的偏移量list.style.transform = `translateY(${offsetTop}px)`;//根据firstIndex,container.height,itemsHeight获得显示项数let lastIndex;//当前的最后一项accumulate = 0;//清空累加器for (let i = firstIndex; i < itemsHeight.length; i++) {accumulate += itemsHeight[i];//高度累加if (accumulate > containerHeight) {//大于容器高度lastIndex = i;break;}}lastIndex ??= itemsHeight.length - 1;//如果没有,赋值为最后一项下标//解决白屏问题lastIndex += 2;if (lastIndex > itemsHeight.length - 1)lastIndex = itemsHeight.length - 1firstIndex -= 2;if (firstIndex < 0)firstIndex = 0;// console.log(firstIndex, lastIndex);const currentArray = data.slice(firstIndex, lastIndex + 1);//裁剪出应该显示的数据//将裁剪的数据显示到list中currentArray.forEach((item, index) => {const div = document.createElement("div");div.innerText = item;list.append(div);//插入后获取高度,更新缓存const trueHeight = div.offsetHeight;itemsHeight[firstIndex + index] = trueHeight;});//根据缓存重新刷新高度refreshAllheight();}render();//第一次渲染数据container.addEventListener("scroll", function () {//绑定滚动事件scrollTop = this.scrollTop;//修改当前scrollToprender();//重新渲染});</script></html>

这里用到了 mockjs 来生成不定高度的模拟数据,可以参考 Mock.js

点我看效果

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

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

相关文章

高德/百度地图: 坐标拾取系统:用经纬度搜索

一,高德地图:坐标拾取器 https://developer.amap.com/tools/picker 如图: 二,百度地图:拾取坐标系统 https://api.map.baidu.com/lbsapi/getpoint/index.html

面试必备之TCP知识

概述 关于TCP的杂乱知识点,不成体系,毕竟TCP真的太复杂。 TCP,Transmission Control Protocol;IP,Internet Protocol,两者共同组成TCP/IP协议族,包含一系列构成互联网基础的网络协议。 OSI七层网络模型图片来自于OSI七层网络模型 OSI七层由于太过严格,所以并没有应用在…

分布式系列之限流组件

概述 在高并发场景下,请求量瞬间到达,后端服务器即使有缓存、集群主备、分库分表、容错降级等措施,也有可能扛不住这请求量,因此可考虑引入限流组件。限流的目的:防止恶意请求流量或流量超出系统承载。 应用场景:网关层校验流量,拦截非法请求,或直接抛弃部分流量(后来…

SQL跟踪

SQL跟踪 如何sql跟踪气死很简单。 1.先打开菜单栏【工具】–【SQL Server Profiler】 2、弹出如下登录界面,选择对应服务器名称,进行身份验证 3.连接成功后,弹出如下界面,选【事件选择】页签,勾选第4、5个选项,并勾选【显示所有列】 4.然后点【1.列筛选器】弹出【编辑筛…

哇!0.8秒启动!Linux快速启动方案分享,全志T113-i国产平台!

本文主要介绍基于创龙科技TLT113-EVM评估板(基于全志T113-i)的系统快速启动方案,适用开发环境如下。Windows开发环境:Windows 7 64bit、Windows 10 64bit 虚拟机:VMware15.5.5 Linux开发环境:Ubuntu18.04.4 64bit U-Boot:U-Boot-2018.07 Kernel:Linux-5.4.61、Linux-RT…

BVS:多强联手,李飞飞也参与的超强仿真数据生成工具,再掀数据狂潮 | CVPR 2024

BEHAVIOR Vision Suite(BVS)是一个新型工具包,旨在系统评估和全面理解计算机视觉模型。研究人员能够在场景、对象和相机级别控制各种参数,有助于创建高度定制的数据集。 来源:晓飞的算法工程笔记 公众号论文: BEHAVIOR Vision Suite: Customizable Dataset Generation via…

9k star 监控系统,100% 国产,推荐了解

前言 监控系统的重要性不言而喻,国内用的最多的应该是 Zabbix 和 Prometheus,其优缺点:Zabbix 是资产管理式,监控数据存在数据库中,擅长设备监控,不擅长微服务和云原生环境的监控;推出时间较早,社区活跃度较高 Prometheus 是云原生环境的监控利器,支持多维度的指标数据…

DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024

Differentiable Model Scaling(DMS)以直接、完全可微的方式对宽度和深度进行建模,是一种高效且多功能的模型缩放方法。与先前的NAS方法相比具有三个优点:1)DMS在搜索方面效率高,易于使用。2)DMS实现了高性能,可与SOTA NAS方法相媲美。3)DMS是通用的,与各种任务和架构…

ArchLinux配置OpenCV C++环境

本文将简单介绍在 ArchLinux 中安装 OpenCV C++ 库并运行一个简单的 OpenCV 程序的过程。 参考:https://github.com/donaldssh/Install-OpenCV 我的环境最新的 ArchLinux KDE Plasma 6 桌面环境 OpenCV 4.10.0 clang 18.1.8 gcc 14.2.1安装 安装以下包: sudo pacman -S hdf5 …

聊聊如何利用ingress-nginx实现应用层容灾

前言 容灾是一种主动的风险管理策略,旨在通过构建和维护异地的冗余系统,确保在面临灾难性事件时,关键业务能够持续运作,数据能够得到保护,从而最大限度地减少对组织运营的影响和潜在经济损失。因此容灾的重要性不言而喻,今天的话题主要是聊下如何利用ingress-nginx实现应…

一个超全的go工具库Lancet

文档官网 https://www.golancet.cn 安装 使用 go1.18 及以上版本的用户,建议安装 v2.x.x。 因为 v2.x.x 应用 go1.18 的泛型重写了大部分函数。 go get github.com/duke-git/lancet/v2使用 go1.18 以下版本的用户,必须安装 v1.x.x。目前最新的 v1 版本是 v1.4.1。 go get git…