1、为什么会有虚拟列表?
在前端开发中,有时候会出现一些不能使用分页来加载数据的情况,因此当我们需要渲染上万条数据的时候,就可能会出现卡顿的情况,这个时候我们就会提出虚拟列表的概念。
2、什么是虚拟列表
虚拟列表就是按需显示的一种实现,即只对可见区域进行渲染,对不可见区域中的数据不渲染或者部分渲染的技术,从而达到极高的渲染性能。假设有1千条数据需要同时渲染,屏幕可见区域的高度是800px,每一个列表的高度是100px,则我们在屏幕中就最多能看到8个列表项。那么在渲染的时候,我们只需要加载可视区域中的那8条数据即可(即只渲染8个dom元素)。
3、基本思路
- 写一个代表可视区域的div容器
- 计算可视区域可以显示的数据条数(可视区域的高度/单条数据的高度)
- 监听滚动,当滚动条变化时,计算被卷起的高度(scrollTop)
- 计算可视区域的起始下标(卷起的高度/单条数据的高度)
- 计算可视区域的结束下标(起始下标+可视区域可以显示的数据条数)
- 取起始下标到结束下标之间的数据,渲染到页面
- 计算起始下标的数据在整个列表中的偏移位置并设置到列表上(transform:translateY())
4、效果图
5、代码
<!DOCTYPE html>
<html>
<head>
<title>js 实现虚拟列表</title>
</head>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
.box {
width: 750px;
height: 900px;
overflow-y: auto;
box-sizing: border-box;
margin: 0 auto;
}
.content-area {
overflow: hidden;
}
li {
height: 100px;
background-color: #eee;
margin-bottom: 10px;
display: flex;
align-items: center;
padding-left: 20px;
border-radius: 12px;
box-sizing: border-box;
}
</style>
<body>
<!-- 可视区域的高度--用户看见的高度(750px) -->
<div class="box">
<!-- 内容真实高度 即200条数据的高度-->
<div class="content-area" id="contentArea">
<ul></ul>
</div>
</div>
<script>
let el = document.querySelector(".box");
let itemHeight = 110; //每个元素的高度(li的高度100 + marginBottom 10)
let pageSize = Math.ceil(el.clientHeight / itemHeight); // 获取一个滚动屏最大可容纳子元素个数(向上取整)
let data = []; //mock数据
let startIndex = 0; //可视区第一行下标
let endIndex = pageSize; //可视区最后一行下标
// 初始化模拟数据
let getData = () => {
for (let i = 0; i < 200; i++) {
data.push({
content: `我是显示的内容${i + 1}`,
});
}
};
// 加载数据并插入到dom页面
let loadData = () => {
let html = "";
let sliceData = data.slice(startIndex, endIndex);
for (let i = 0; i < sliceData.length; i++) {
html += `
<li class="item">
<p>${sliceData[i].content}</p>
</li>`;
}
el.querySelector("#contentArea ul").innerHTML = html;
};
// 更新DOM
let updateHtml = () => {
let sliceData = data.slice(startIndex, endIndex);
let itemAll = el.querySelectorAll(".item");
for (let i = 0; i < sliceData.length; i++) {
itemAll[i].querySelector("p").innerHTML = sliceData[i].content;
}
};
// 滑动监听
el.addEventListener("scroll", function () {
let scrollTop = el.scrollTop; // 滚动高度
startIndex = Math.ceil(scrollTop / itemHeight); // 重新计算开始的下标,div顶部卷起来的长度除以列表元素的高度
endIndex = startIndex + pageSize;
updateHtml(); // 重新更新dom
el.querySelector("#contentArea ul").style.transform =
"translateY(" + startIndex * itemHeight + "px)";
});
let init = () => {
getData();
loadData();
document.getElementById("contentArea").style.height =
itemHeight * data.length + "px"; // 占位dom的高度
};
// 页面初始化调用
init();
</script>
</body>
</html>