【js】无限虚拟列表的原理及实现

什么是虚拟列表

虚拟列表是长列表按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。

简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下:

视口容器元素: 定义固定宽高的元素,该区域限制无限虚拟列表的可视区域大小
可滚动区域元素: 宽高为父元素的100%,纵向超出可滚动
内容区域元素: 宽度100%,高度auto,用于呈放渲染的部分列表项,撑开可滚动区域

在这里插入图片描述

实现思路

实现虚拟列表就是,当用户滚动时,动态改变可视区域内的渲染内容

滚动时 =》

  • 监听可滚动区域滚动事件

变化 =》

  • 内容区域渲染的列表数据变化

  • 内容区域一直显示在视口上

  • 滚动区域的高度增加

在这里插入图片描述

具体实现

想明白思路之后,根据思路,一步步进行

首先创建四个html元素

分别定义类名为:container、list_scroll、list、item,结构如下

<div class="container"><div class="list_scroll"><div class="list"><div class="item">1</div></div></div>
</div>

通过类名定义样式


/* 最外层容器,宽高固定列表视口大小 */
.container{width:500px;height: 800px;border: 1px solid #f80c0c;margin: auto; /* 居中 */
}/* 可滚动容器,占最外层容器宽高100% 能被显示的列表撑开 */
.list_scroll{width: 100%;height: 100%;overflow: auto; /* 超出滚动 */background-color: antiquewhite;
}
/* 虚拟列表容器,用于展示长列表位于视口区域的部分项 */
.list{width: 90%;margin: auto; /* 居中 */
}/* 子项 */
.item{width: 100%;border: 1px solid #000;box-sizing: border-box;display: flex;justify-content: center;align-items: center;
}

创建好元素之后,开始写js逻辑实现

准备操作:

需要两个数组:源数据、渲染列表数据和视口展示列表的长度,即可展示的最大数量
可展示的最大数量: 可通过 “视口容器的高度 / item的高度” 获取,默认item高度固定
渲染列表数据:通过对源数据进行切割获取,所以还需要知道切割数组的开始位置、结束位置

// 获取容器和列表元素
const listScroll = document.querySelector('.list_scroll')
const list = document.querySelector('.list')// 源数据
const dataSource = []
// 渲染数据=> 通过定义首位index截取源数据
let renderData = []
// item的高度
const itemHeight = 50
// listScroll容器能够显示的最大数量
// +2 撑开listScroll容器使其具有滚动条
const maxCount = Math.floor(listScroll.clientHeight / itemHeight) + 2
// 开始位置索引
let startIndex = 0
// 结束位置索引
let endIndex = 0

获取源数据

// 源数据
function GetData () {for (let i = 0; i < 200; i++) {dataSource.push(i)}
}

计算开始位置和结束位置

开始位置:初始为0,当滚动到第二个item时,从0 =》1,滚动到第三个item,1 =》2 …
此时说明:滚动条从顶部初始位置到当前位置(第n个item)的距离,就是滑出视口的n-1个item的高度

// 计算开始位置和结束位置索引
function ComputePointerPosition () {const end = startIndex + maxCountendIndex = dataSource[end] ? end : dataSource.length
}

根据开始位置和结束位置,截取渲染数据

// 截取渲染数据
function GetRenderData () {renderData = dataSource.slice(startIndex, endIndex)
}

万事具备,只欠东风,开始渲染到页面

// 渲染
function Render () {// 计算开始和结束位置ComputePointerPosition()// 获取数据GetRenderData()// 将截取的渲染数据生成动态的item元素,填充到list内容元素list.innerHTML = renderData.map(item => `<div class="item" style="height: ${itemHeight}px">${item}</div>`).join('')
}

监听可滚动区域的滚动事件

// 监听滚动事件
listScroll.addEventListener('scroll', ScrollHandle)// 监听listOut滚动事件
function ScrollHandle () {// 更新开始位置索引:滚动的距离 / 每个元素的高度startIndex = Math.floor(listScroll.scrollTop / itemHeight)// 更新位置,重新渲染Render()
}

运行
感觉怪怪的,并且没一会就到底了
在这里插入图片描述
仔细观察你就会发现,这有2个问题

  1. 当第一个item滑出可视区域之后,右侧的dom结构渲染是正确的第一个item变为1,但是页面上看到是第一个是2;当再次向下滚动一个元素之后,右侧dom第一个item为2,但是页面上看到的第一个确是4
  2. 可滚动区域的高度并没有随着滚动一直增加,没几下就触底了,没有办法再继续监听了,也就没有办法继续更新数据了

第一个问题产生的原因就是,随着可滚动区域的滚动,内容区域数据变化,但也随着滚动滑出了可视区域,如图
在这里插入图片描述
所以我们要为内容区域设置transform: translateY(值),通过动态改变这个值,使内容区域顶部与可视区域顶部齐平,如图
在这里插入图片描述
当我们为内容区域增加transform: translateY(值)时, 可滚动区域的高度是会随着增加的,如图
在这里插入图片描述

每次向下滚动一个元素,列表会向上移动一个元素的位置,startIndex表示已经上移到的元素的个数,itemHeight表示每个元素的高度,所以我们将ScrollHandle事件改成如下

// 监听listOut滚动事件
function ScrollHandle () {// 更新位置,重新渲染Render()// 测试发现每次向下滚动一个元素,列表会向上移动一个元素的位置,所以增加transform属性,使列表位置向下移动一个元素的位置// startIndex表示已经上移到的元素的个数,itemHeight表示每个元素的高度list.style.transform = `translateY(${startIndex * itemHeight}px)`
}

效果如图
在这里插入图片描述

优化

不难发现,随着滚动条到滚动,dom一直在刷新,太耗性能了,此时我们需要对滚动事件进行节流。
节流的方式有很多,最常见的就是计时器,但此处我们不需要计时器,只需要将satrtIndex
进行缓存,比较二者是否一致,不一致说明需要重新渲染了

// 记录到的位置索引
let pointerIndex = 0// 监听listOut滚动事件
function ScrollHandle () {// 更新开始位置索引:滚动的距离 / 每个元素的高度startIndex = Math.floor(listScroll.scrollTop / itemHeight)// 一致不做渲染if (pointerIndex === startIndex) returnpointerIndex = startIndex// 更新位置,重新渲染Render()// 测试发现每次向下滚动一个元素,列表会向上移动一个元素的位置,所以增加transform属性,使列表位置向下移动一个元素的位置// startIndex表示已经上移到的元素的个数,itemHeight表示每个元素的高度list.style.transform = `translateY(${startIndex * itemHeight}px)`
}

在这里插入图片描述
加载更多

到此,虚拟列表的实现已经完成,源数据是长度为200的长列表。我们可以判断是否到底,来加载更多,可通过已加载的数组的总长度 - 开始位置是否 小于 可展示的最大数量,此时需要加载更多数据

// 监听listOut滚动事件
function ScrollHandle () {// 更新开始位置索引:滚动的距离 / 每个元素的高度startIndex = Math.floor(listScroll.scrollTop / itemHeight)if (pointerIndex === startIndex) returnpointerIndex = startIndex// 更新位置,重新渲染Render()if (dataSource.length - startIndex >= maxCount) {// 测试发现每次向下滚动一个元素,列表会向上移动一个元素的位置,所以增加transform属性,使列表位置向下移动一个元素的位置// startIndex表示已经上移到的元素的个数,itemHeight表示每个元素的高度list.style.transform = `translateY(${startIndex * itemHeight}px)`} else {// 滑动到底部 加载增更多数据GetData()}
}

完整代码

// 获取容器和列表元素
const listScroll = document.querySelector('.list_scroll')
const list = document.querySelector('.list')// 源数据
const dataSource = []
// 渲染数据=> 通过定义首位index截取源数据
let renderData = []
// item的高度
const itemHeight = 50
// listScroll容器能够显示的最大数量
// +2 撑开listScroll容器使其具有滚动条
const maxCount = Math.floor(listScroll.clientHeight / itemHeight) + 2
// 开始位置索引
let startIndex = 0
// 结束位置索引
let endIndex = 0// 源数据
function GetData () {for (let i = 0; i < 200; i++) {dataSource.push(i)}
}// 计算开始位置和结束位置索引
function ComputePointerPosition () {const end = startIndex + maxCountendIndex = dataSource[end] ? end : dataSource.length
}// 截取渲染数据
function GetRenderData () {renderData = dataSource.slice(startIndex, endIndex)
}// 渲染
function Render () {// 计算开始和结束位置ComputePointerPosition()// 获取数据GetRenderData()// 将截取的渲染数据生成动态的item元素,填充到list内容元素list.innerHTML = renderData.map(item => `<div class="item" style="height: ${itemHeight}px">${item}</div>`).join('')
}// 记录到的位置索引
let pointerIndex = 0// 监听listOut滚动事件
function ScrollHandle () {// 更新开始位置索引:滚动的距离 / 每个元素的高度startIndex = Math.floor(listScroll.scrollTop / itemHeight)if (pointerIndex === startIndex) returnpointerIndex = startIndex// 更新位置,重新渲染Render()if (dataSource.length - startIndex >= maxCount) {// 测试发现每次向下滚动一个元素,列表会向上移动一个元素的位置,所以增加transform属性,使列表位置向下移动一个元素的位置// startIndex表示已经上移到的元素的个数,itemHeight表示每个元素的高度list.style.transform = `translateY(${startIndex * itemHeight}px)`} else {// 滑动到底部 加载增更多数据GetData()}
}function init () {// 获取数据GetData()Render()// 监听滚动事件listScroll.addEventListener('scroll', ScrollHandle)
}init()

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

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

相关文章

python专业版破解激活(超详细)

python专业版破解激活 1.下载pycharm应用程序 这里我使用的版本是pycharm-professional-2023.3.2 下载pycharm程序的连接为&#xff1a; 百度网盘 请输入提取码 提取码为&#xff1a;nym0 2.安装 选择安装路径 下一步 这里全选 下一步 这里直接点击安装就可&#xff0c;其…

Opencv(2)深浅拷贝与基本绘图(c++python

Opencv(2)深浅拷贝与基本绘图 文章目录 Opencv(2)深浅拷贝与基本绘图三、深浅拷贝四、HSV色域(1).意义(2).cvtColor()(3).inRange()(4).适应光线 三、深浅拷贝 浅拷贝是指当图像之间进行赋值时&#xff0c;图像数据并未发生复制&#xff0c;而是两个对象都指向同一块内存块。 …

光伏气象站:实现自动化、高精度的气象监测

型号推荐&#xff1a;云境天合 TH-FGF9】光伏气象站是一种基于光伏技术的气象监测设备&#xff0c;它利用太阳能转化为电能&#xff0c;为气象站提供持续的电力供应&#xff0c;并实现自动化、高精度的气象监测。 光伏气象站的工作原理可以分为以下几个部分&#xff1a; 光伏发…

SpringCloud Nacos安装

1. Nacos的下载&#xff1a;下载的是1.4的版本。 2. Nacos的安装&#xff1a; startup.cmd -m standalone 以单机模式启动Nacos。 登录的账号密码 都是nacos。

Android LruCache源码分析

文章目录 Android LruCache源码分析概述LruCache和LinkedHashMap关系源码分析属性写入数据读取数据删除缓存 Android LruCache源码分析 概述 LruCache&#xff08;Least Recently Used Cache&#xff0c;最近最少使用缓存&#xff09;是 Android 中的一种缓存机制。 根据数据…

MySQL 索引原理以及 SQL 优化

索引 索引&#xff1a;一种有序的存储结构&#xff0c;按照单个或者多个列的值进行排序。索引的目的&#xff1a;提升搜索效率。索引分类&#xff1a; 数据结构 B 树索引&#xff08;映射的是磁盘数据&#xff09;hash 索引&#xff08;快速锁定内存数据&#xff09;全文索引 …

ChatGPT调教指南 | 咒语指南 | Prompts提示词教程(一)

在我们开始探索人工智能的世界时&#xff0c;了解如何与之有效沉浸交流是至关重要的。想象一下&#xff0c;你手中有一把钥匙&#xff0c;可以解锁与OpenAI的GPT模型沟通的无限可能。这把钥匙就是——正确的提示词&#xff08;prompts&#xff09;。无论你是AI领域的新手&#…

SpringCloud全家桶---常用微服务组件(1)

注册中心: *作用: 服务管理 Eureka(不推荐)[读音: 优瑞卡] Nacos(推荐) Zookeeper [读音: 如k波] Consul [读音:康寿] **注册中心的核心功能原理(nacos)** 服务注册: 当服务启动时,会通过rest接口请求的方式向Nacos注册自己的服务 服务心跳: NacosClient 会维护一个定时心跳持…

Sora背后的论文(1):使用 lstms 对视频展现进行无监督学习

之前那篇《Sora背后的32篇论文》发出后&#xff0c;大家都觉得不错&#xff0c;有很多小伙伴都开始啃论文了。 那么我就趁热打铁&#xff0c;把这32篇论文的通俗解读版贴一下。 从去年开始&#xff0c;我基本上形成了一个思维方式&#xff0c;任何事情做之前先看看 有没有好的…

个人博客系列-环境配置-gitee(2)

注册gitee账户 地址&#xff1a;https://gitee.com/ 此步骤省略 新建仓库 执行以下命令 即可 拉取代码 创建目录 mkdir myCode && cd myCode 登录gitee找到项目&#xff0c;点击克隆&#xff0c;拉取代码 连接远程仓库命令 git remote add origin 仓库地址http…

MariaDB落幕和思考

听过MySQL的基本也都知道 MariaDB。MariaDB由MySQL的创始人主导开发&#xff0c;他早前曾以10亿美元的价格&#xff0c;将自己创建的公司MySQL AB卖给了SUN&#xff0c;此后&#xff0c;随着SUN被甲骨文收购&#xff0c;MySQL的所有权也落入Oracle的手中。传闻MySQL的创始人担心…

创建型设计模式 - 原型设计模式 - JAVA

原型设计模式 一 .简介二. 案例三. 补充知识 前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff0c;关注我&#xff0c;接下来还会持续更新。 作者&#xff1a;神的孩子都在歌唱 一 .简介 原型模式提供了一种机制&#xff0c;可以将原始对象复制到新对象&#xff0…

[TCP] TCP/IP 基础知识词典(2)

我想统计一下&#xff0c;TCP/IP 尤其是TCP协议&#xff0c;能搜到的常见的问题&#xff0c;整理起来&#xff0c;关键词添加在目录中&#xff0c;便于以后查阅。 目前预计整理共3篇&#xff1a; [TCP] TCP/IP 基础知识问答 &#xff1a;基础知识 [TCP] TCP/IP 基础知识问答&…

游戏平台如何定制开发?

随着科技的飞速发展和互联网的普及&#xff0c;游戏平台已成为人们休闲娱乐的重要选择。为了满足用户多样化的需求&#xff0c;游戏平台的定制开发显得尤为重要。本文将探讨游戏平台定制开发的过程、关键要素以及注意事项&#xff0c;为有志于涉足此领域的开发者提供参考。 一、…

ApexRBp在线粒子传感器在电动汽车电池制造的应用

电动汽车电池的崛起与颗粒污染的挑战 随着电动汽车&#xff08;EV&#xff09;市场的迅速扩张&#xff0c;对高性能锂离子电池的需求也急剧增加。这些电池不仅是EV的心脏&#xff0c;更是推动其前行的核心动力。然而&#xff0c;在电池制造的每一个环节&#xff0c;都需要对多…

【Python笔记-设计模式】适配器模式

一、说明 适配器模式是一种结构型模式&#xff0c;它使接口不兼容的对象能够相互合作 (一) 解决问题 主要解决接口不兼容问题 (二) 使用场景 当系统需要使用现有的类&#xff0c;但类的接口不符合需求时当需要一个统一的输出接口&#xff0c;但输入类型不可预知时当需要创…

查询数据库的编码集Oracle,MySQL

1、查询数据库的编码集Oracle,MySQL 1.1、oracle select * from v$nls_parameters where parameterNLS_CHARACTERSET; 查询版本&#xff1a;SELECT * FROM v$version 2、MySQL编码集 SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SC…

【了解机器学习的定义与发展历程】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 简述概要知识图谱 简述概要 了解机器学习的定义与发展历程 知识图谱 机器学习&#xff08;Machine Learning&#xff0c;ML&#xff09;是一门跨学科的学科&#xff0c;它使用计算机模拟或实现人类学习行为&#xff0c;通…

路由器的端口映射如何设置?

路由器的端口映射设置是网络连接中常用的配置功能&#xff0c;通过将外部网络访问请求映射到内部设备&#xff0c;实现局域网内设备的远程访问。本文将介绍如何进行路由器的端口映射设置&#xff0c;并以【天联】组网产品为例进行说明。 【天联】组网产品介绍 【天联】组网是一…

css3d制作正方体

使用css3d技术 &#xff0c;制作一个可以动态动画的正方体模型 效果图&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html> <head><style>/* 设置高度宽度100%并且左右居中、上下居中 */html,body {width: 100%;height: 100%;display: flex…