什么是虚拟列表
虚拟列表是长列表按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。
简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下:
视口容器元素: 定义固定宽高的元素,该区域限制无限虚拟列表的可视区域大小
可滚动区域元素: 宽高为父元素的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个问题
- 当第一个item滑出可视区域之后,右侧的dom结构渲染是正确的第一个item变为1,但是页面上看到是第一个是2;当再次向下滚动一个元素之后,右侧dom第一个item为2,但是页面上看到的第一个确是4
- 可滚动区域的高度并没有随着滚动一直增加,没几下就触底了,没有办法再继续监听了,也就没有办法继续更新数据了
第一个问题产生的原因就是,随着可滚动区域的滚动,内容区域数据变化,但也随着滚动滑出了可视区域,如图
所以我们要为内容区域设置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()