持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2
天,点击查看活动详情
虚拟列表是前端解决海量数据展示的一种解决方案。
当我们需要展示万条,百万条数据时。如果使用传统的分页向下展示。随着数据量的增多,HTML节点也会增加,HTML节点越多,重绘
和重排
的花销也会增大,慢慢地会让你的页面变得非常的慢。
那么为什么使用虚拟列表就能解决这个问题呢?
虚拟列表的原理
虚拟列表的原理其实就是:当我们查询出大量数据时,只展示当前可视区域的数据,其他的数据只在滚动到数据的页数时才展示。如图所示:
我们可以预先加载几页数据,当我们滚动到预加载的页面时,加载下(上)一页数据,并删除上(下)一页数据。这样无论我们怎么滚动,页面中展示的数据量始终保持固定的。
实现虚拟列表
了解完它的原理,接下来就是需要实现这个虚拟列表,这里可以提供两种方式解决问题~
- 监听列表容器的滚动,通过滚动对可视列表进行控制
- 使用
ResizeObserver
监听内容区域的改变,从而控制可视列表。但其方法是实验性的。
因为ResizeObserver
是实验性的API,不推荐在生产环境中使用,所以我们这里第一种方法:监听滚动来实现。
具体的实现步骤为:
- 创建容器并监听容器滚动
- 获取容器高度和每个列表的高度
- 计算触发下(上)一页触发的滚动距离
- 计算留白的高度(重要)
其中,计算留白的高度是虚拟列表最重要的一环,因为当元素隐藏时,为了保证展示列表不出现塌陷,需要使用padding
将容器撑高。
虚拟列表有定高
和不定高
两种。
- 定高就是每一个item固定高度,我们在计算滚动距离的时候就会比较轻松。
- 但是很多情况下,列表的item高度是不固定的,这时我们就要比定高多一步:计算每一页渲染后动态计算的总高度。
定高虚拟列表
首先新建一个html文件,创建一个box容器,固定容器高度并监听滚动
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>虚拟列表</title>
<meta charset="utf8" />
<style>
* {
padding: 0;
margin: 0;
}
body {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
#box {
width: 300px;
height: 500px;
background: purple;
overflow-y: scroll;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="box">
<div id="box_container"></div>
</div>
</body>
<script lang="text/javascript" src="./script.js"></script>
</html>
// script.js
(() =>{
// 定义初始化数据
let page = 1; // 页码
let size = 20; // 每页条数
const height = 50; // 每条高度
const preLoadNum = 3; // 同时展示载页数
const boxHeight = 500; // 容器高度
let paddingBottom = 50; // 底部留白高度
let listArr = []; // 用于存放列表数据
const box = document.querySelector("#box");
// 监听滚动
box.addEventListener("scroll", (e) => {}, false)
})()
定义完一些基础的初始信息之后,接下来写一个获取数据的方法
// script.js
function createItem(page = 1, size = 10) {
const fragment = document.createDocumentFragment()
const box = document.createElement("div");
box.className = `page_${page}`; // 给每页容器定一个类名,后面根据类名进行容器删除
for (let i = 0; i < size; i++) {
const element = document.createElement("div");
element.style.width = "100%";
element.style.height = "50px";
element.style.color = "#fff";
element.className = `item_${page * (i + 1)}`;
element.innerText = `我是item——${((page - 1) * size) + i + 1}`;
box.appendChild(element);
}
fragment.appendChild(box);
return {fragment, box};
}
回到立即执行函数中,进入页面时立即执行一遍
// script.js
(() =>{
// 定义初始化数据
let page = 1; // 页码
let size = 20; // 每页条数
const height = 50; // 每条高度
const preLoadNum = 3; // 同时展示载页数
const boxHeight = 500; // 容器高度
let listArr = []; // 用于存放列表数据
const box = document.querySelector("#box");
const boxContainer = document.querySelector("#box_container");
const {fragment, box: boxList} = createItem(page, size)
listArr.push(boxList); // 将数据放入列表中
boxContainer.appendChild(fragment); // 载入初始数据
// 监听滚动
box.addEventListener("scroll", (e) => {
}, false)
})()
接下来就需要将计算滚动高度去获取并展示数据
box.addEventListener("scroll", (e) => {
const scrollTop = e.target.scrollTop
if (scrollTop >= nextHeight) {
page++;
// 顶部留白高度
paddingTop = (page - preLoadNum) * (size * height) + paddingBottom;
// 触发下一页的高度
nextHeight = (page - 1) * (size * height) + paddingBottom + boxHeight;
let fragment;
// 判断该页数据是否已经存在列表中,存在则无须获取新的
if (!listArr[page - 1]) {
const {fragment: element, box: boxList} = createItem(page, size)
fragment = element;
listArr.push(boxList);
} else {
fragment = listArr[page - 1]
}
boxContainer.appendChild(fragment);
// 判断是否存在需要删除的列表
const hideElem = document.querySelector(`.page_${page - preLoadNum}`);
if (hideElem) {
// 如果有则删除列表并新增留白高度
boxContainer.removeChild(hideElem);
boxContainer.style.paddingTop = `${paddingTop}px`;
}
} else if (
scrollTop <= (page - preLoadNum + 1) * size * height + paddingBottom && page > preLoadNum
) {
page--;
// 顶部留白高度
paddingTop = (page - preLoadNum) * (size * height) + paddingBottom;
// 触发下一页的高度
nextHeight = (page - 1) * (size * height) + paddingBottom + boxHeight;
// 取出上一页的数据并插入
const fragment = listArr[page - preLoadNum];
boxContainer.insertBefore(fragment, boxContainer.childNodes[0]);
const hideElem = document.querySelector(`.page_${page + 1}`);
if (hideElem) {
// 删除下面的列表并减去顶部留白
boxContainer.removeChild(hideElem);
boxContainer.style.paddingTop = `${paddingTop}px`;
}
}
}, false)
大功告成!来测试一下效果
不定高虚拟列表
对于不定高的虚拟列表,我们需要计算每一页的总高度,或者将每一页的每一项高度都加上然后累加去,因为我们每一页都用了一个容器来将他们保存起来,并且容器是自动撑高的,所以,只需要获取每一页容器的高度即可。修改一下上面的代码:
function createItem(page = 1, size = 10) {
const fragment = document.createDocumentFragment()
const box = document.createElement("div");
box.className = `page_${page}`;
for (let i = 0; i < size; i++) {
const element = document.createElement("div");
// 高度随机
let height = Math.ceil(Math.random() * 5) * 50
element.style.width = "100%";
element.style.height = `${height}px`;
element.style.color = "#fff";
element.className = `item_${page * (i + 1)}`;
element.innerText = `我是item——${((page - 1) * size) + i + 1} \n 高度——${height}`;
box.appendChild(element);
}
fragment.appendChild(box);
return {fragment, box};
}
(() => {
let page = 1;
let size = 20;
let height = 50;
let preLoadNum = 3;
let boxHeight = 500;
let paddingBottom = 50;
const box = document.querySelector("#box");
const boxContainer = document.querySelector("#box_container");
let listArr = [];
let isGetting = false;
const {fragment, box: boxList} = createItem(page, size)
boxContainer.appendChild(fragment);
// 获取第一页的总高度
const listHeight = document.querySelector(`.page_${page}`).clientHeight;
listArr.push({boxList, height: listHeight});
let paddingTop = 0;
// 触发渲染下一页的条件
let nextHeight = paddingTop + listHeight + paddingBottom - boxHeight;
boxContainer.style.paddingBottom = `${paddingBottom}px`;
boxContainer.style.paddingTop = `${paddingTop}px`;
box.addEventListener("scroll", (e) => {
const scrollTop = e.target.scrollTop
if (scrollTop >= nextHeight) {
if (isGetting) return;
isGetting = true;
page++;
let fragment;
let pushObj;
if (!listArr[page - 1]) {
const {fragment: element, box: boxList} = createItem(page, size)
fragment = element;
pushObj = { boxList };
} else {
const { boxList, height } = listArr[page - 1]
fragment = boxList;
}
boxContainer.appendChild(fragment);
// 更新顶部留白
paddingTop = (listArr.filter((_, index) => index < page - preLoadNum)).map((val) => val.height).reduce((a,b) => (a + b), 0);
// 更新下一页触发条件
nextHeight = boxContainer.clientHeight - boxHeight - paddingBottom;
if (pushObj) {
const listHeight = document.querySelector(`.page_${page}`).clientHeight;
pushObj.height = listHeight;
listArr.push(pushObj)
}
const hideElem = document.querySelector(`.page_${page - preLoadNum}`);
if (hideElem) {
boxContainer.removeChild(hideElem);
boxContainer.style.paddingTop = `${paddingTop}px`;
}
// 渲染上一页条件
} else if (scrollTop <= nextHeight - listArr[listArr.length - 1].height && page > preLoadNum) {
page--;
// 拉取上一页数据并渲染
const { boxList } = listArr[page - preLoadNum];
boxContainer.insertBefore(boxList, boxContainer.childNodes[0]);
const hideElem = document.querySelector(`.page_${page + 1}`);
// 删掉最下面一页
if (hideElem) {
// 更新顶部留白
paddingTop = (listArr.filter((_, index) => index < page - preLoadNum)).map((val) => val.height).reduce((a,b) => (a + b), 0);
boxContainer.style.paddingTop = `${paddingTop}px`;
boxContainer.removeChild(hideElem);
// 更新下一页渲染条件
nextHeight = boxContainer.clientHeight - boxHeight - paddingBottom;
}
}
isGetting = false;
}, false);
})()
最后实现效果
总结
本文主要介绍了
- 虚拟列表的原理
- 虚拟列表的实现步骤
- 定高的虚拟列表实现
- 不定高的虚拟列表实现