【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 + maxCount
  endIndex = 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) return

  pointerIndex = 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) return

  pointerIndex = 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 + maxCount
  endIndex = 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) return

  pointerIndex = 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()

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

KDD 2023 图神经网络方向论文总结

ACM SIGKDD&#xff08;国际数据挖掘与知识发现大会&#xff0c;KDD&#xff09;是数据挖掘领域历史最悠久、规模最大的国际顶级学术会议&#xff0c;也是首个引入大数据、数据科学、预测分析、众包等概念的会议。今年&#xff0c;第29届 KDD 大会在美国加州长滩举行&#xff0…

冒泡排序法的名字由来,排序步骤是什么,最坏情况下的排序次数如何计算得来的呢?

问题描述&#xff1a;冒泡排序法的名字由来&#xff0c;排序步骤是什么&#xff0c;最坏情况下的排序次数如何计算得来的呢&#xff1f; 问题解答&#xff1a; 冒泡排序法的名字来源于排序过程中较大的元素会像气泡一样逐渐“冒”到序列的顶端&#xff0c;而较小的元素则会逐…

代码随想录算法训练营第四十天 343. 整数拆分、 96.不同的二叉搜索树

代码随想录算法训练营第四十天 | 343. 整数拆分、 96.不同的二叉搜索树 343. 整数拆分 题目链接&#xff1a;343. 整数拆分 - 力扣&#xff08;LeetCode&#xff09; 例如 n 10, 可以拆分为 3 * dp[7] 。因为dp[7]之前已经计算过最大 3 * 4&#xff0c; 所以dp[10] 3 * 3 …

Microsoft 365自定义安装软件

如图&#xff0c;在安装类型的步骤的时候&#xff0c;可以勾选自己想要的软件&#xff08;而非一股脑儿的安装一大堆自己不需要的&#xff09;。

AI绘画巅峰对决:Stable Diffusion 3与DALL·E 3原理深度比较

最近&#xff0c;Stable Diffusion 3 的预览版已经亮相啦&#xff01; 虽然这个AI绘画模型还没全面上线&#xff0c;但官方已经开启预览申请通道了。 https://stability.ai/stablediffusion3 而且好消息是&#xff0c;后面还会推出开源版本哦&#xff01; 这个模型套件真的…

五种多目标优化算法(MOAHA、MOGWO、NSWOA、MOPSO、NSGA2)性能对比(提供MATLAB代码)

一、5种多目标优化算法简介 多目标优化算法是用于解决具有多个目标函数的优化问题的一类算法。其求解流程通常包括以下几个步骤&#xff1a; 1. 定义问题&#xff1a;首先需要明确问题的目标函数和约束条件。多目标优化问题通常涉及多个目标函数&#xff0c;这些目标函数可能…

基于SpringBoot的产业园区智慧公寓管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

这才开工没几天收到Offer了,简历改的好,找工作没烦恼。

喜报喜报 这才开工没几天&#xff0c;就收到了喜报&#xff01; 就像上面截图中所说的一样&#xff1a;简历改了真的有用。 我也和大家分享一下优化简历的技巧&#xff0c;希望对大家有帮助&#xff0c;把握住金三银四的机会&#xff0c;都能顺利上岸&#xff0c;升职加薪&am…

多数pythoneer只知有列表list却不知道python也有array数组

数组和列表 Python中数组和列表是不同的&#xff0c;我敢断言大多数的pythoneer只知道有列表list&#xff0c;却不知道python也有array数组。列表是一个包含不同数据类型的元素集合&#xff0c;而数组是一个只能含相同数据类型的元素集合。 Python的array库是一个提供数组操作…

Android 广播的基本概念

一.广播简介 Broadcast是安卓四大组件之一。安卓为了方便进行系统级别的消息通知&#xff0c;引入了一套广播消息机制。打个比方&#xff0c;记得原来在上课的时候&#xff0c;每个班级的教室里都会装有一个喇叭&#xff0c;这些喇叭都是接入到学校的广播室的&#xff0c;一旦…

【初始RabbitMQ】交换机的实现

交换机概念 RabbitMQ消息传递模型的核心思想就是&#xff1a;生产者生产的消息从不会直接发送到队列。实际上&#xff0c;通常生产者不知道这些消息会传递到那些队列中 相反&#xff0c;生产者只能将消息发送到交换机&#xff0c;交换机的工作内容也很简单&#xff0c;一方面…

网络安全8-11天笔记

内容安全&#xff1a; 攻击可能只是一个点&#xff0c;防御需要全方面进行。 IAE引擎&#xff1a; DFI和DPI技术&#xff1a;深度检测技术 DPI——深度包检测技术&#xff1a;主要针对完整的数据包&#xff08;数据包分片&#xff0c;分段需要重组&#xff09;&#xff0c;之…

Linux--自定义shell

shell shell就是操作系统提供给用户与操作系统进行交互的命令行界面。它可以理解为一个用户与操作系统之间的接口&#xff0c;用户可以通过输入命令来执行各种操作&#xff0c;如文件管理、进程控制、软件安装等。Shell还可以通过脚本编程实现自动化任务。 常见的Unix系统中使…

http相关概念以及apache的功能(最详细讲解!!!!)

概念 互联网&#xff1a;是网络的网络&#xff0c;是所有类型网络的母集 因特网&#xff1a;世界上最大的互联网网络 万维网&#xff1a;www &#xff08;不是网络&#xff0c;而是数据库&#xff09;是网页与网页之间的跳转关系 URL:万维网使用统一资源定位符&#xff0c;…

图片如何降低kb?这个方法很方便

图片体积过大的话&#xff0c;有两种最简单的方法可以解决&#xff0c;最直接的就是压缩图片大小&#xff0c;降低图片kb&#xff0c;再就是修改图片尺寸让图片体积变小&#xff0c;这两种操作方式都可以在本文介绍的这款图片处理工具中完成&#xff0c;图片压缩对我们来说最主…

利用netty手写rpc框架

前言&#xff1a;利用netty异步事件驱动的网络通信模型&#xff0c;来实现rpc通信 一、大致目录结构&#xff1a; 二、两个端&#xff1a;服务端&#xff08;发布&#xff09;&#xff0c;客户端&#xff08;订阅消费&#xff09;&#xff0c;上代码&#xff1a; 1.服务端&am…

深入学习TS的高阶语法(泛型、类型检测、内置工具)

文章目录 概要一.TS的类型检测1.鸭子类型2.严格的字面量类型检测 二.TS的泛型1.基本使用2.传递多个参数3.泛型接口4.泛型类5.泛型约束6.映射类型&#xff08;了解&#xff09; 三.TS的知识扩展1.模块的使用-- 内置类型导入 2.类型的查找3.第三方库的类型导入4.declare 声明文件…

Javase-数组

文章目录 1.1 为什么要使用数组1.2 数组的定义及初始化1.3 数组的使用1.4 遍历数组1.5 数组在内存中的存储分析1.6 数组的传参1.7 数组的拷贝 1.1 为什么要使用数组 假设现在有一个任务,要你存储5个同学的学习成绩(double类型),这时候我们可以写出来 double score1 90.4…等五…

谷粒商城-nginx搭建域名访问环境性能压测

nginx搭建域名访问环境 正向代理与反向代理 正向代理&#xff1a;客户端向代理服务器发请求并指定目标服务器&#xff0c;代理向目标服务器转交请求并将获得的内容返回给客户端。 反向代理&#xff1a;用户直接访问反向代理服务器就可以获得目标服务器的资源。反向代理服务器…

Redis能保证数据不丢失吗?

引言 大家即使没用过Redis&#xff0c;也应该都听说过Redis的威名。 Redis是一种Nosql类型的数据存储&#xff0c;全称Remote Dictionary Server&#xff0c;也就是远程字典服务器&#xff0c;用过Dictionary的应该都知道它是一种键值对&#xff08;Key-Value&#xff09;的数…