【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()

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
JavaScript懒加载(Lazy Loading)是一种优化网页性能的技术,它可以延迟加载某些资源,比如图片、视频、音频等,直到它们需要被展示在屏幕上时再进行加载。这样可以减少初始页面的加载时间,提高用户体验。 懒加载的原理是通过JavaScript动态地向页面中添加元素,这些元素在初始时不会加载,当用户滚动页面,元素出现在可视区域内时再进行加载。这个过程可以通过监听滚动事件来实现。 以下是一个简单的懒加载实现的示例代码: 1. 在HTML中添加需要延迟加载的元素,比如图片 ```html <img class="lazy" data-src="example.jpg" alt="Example"> ``` 2. 使用JavaScript监听页面滚动事件,在元素出现在可视区域内时添加src属性来触发加载 ```javascript function lazyLoad() { var lazyImages = document.querySelectorAll('.lazy'); lazyImages.forEach(function(img) { if (img.getBoundingClientRect().top < window.innerHeight) { img.src = img.dataset.src; img.classList.remove('lazy'); } }); } document.addEventListener('scroll', lazyLoad); ``` 在这个示例中,我们使用了querySelectorAll()方法来获取所有有“lazy”类的元素,然后在滚动事件中遍历这些元素。如果元素的位置小于窗口的可视高度,就将data-src属性赋值给src属性来触发加载,并将“lazy”类从元素中移除。 注意,这只是一个简单的懒加载实现示例,实际应用中需要考虑更多的细节,比如优化滚动事件的性能、处理图片的加载失败等等。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麻辣翅尖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值