一个刚入行没多久的菜比前端写的很简单的虚拟滚动,欢迎大佬们批评指正
先上效果
html部分
html
<div class="container">
<div class="scroll-container" id="scroll-container">
<div class="placeholder"></div>
<div
class="scroll-content"
id="scroll-content"
style="height: 1000000px"
>
</div>
</div>
</div>
css部分
css
.container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.placeholder {
width: 100%;
}
.scroll-container {
width: 300px;
height: 500px;
border: 1px solid #ccc;
overflow-y: scroll;
position: relative;
}
.scroll-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
scroll-container
为列表的容器 scroll-content
为列表的可视部分,绝对定位展示在容器的最上层(表面) placeholder
为一个实际上撑起列表高度的div(实际)
js部分
渲染前准备
js
const scrollContainer = document.getElementById("scroll-container");
const scrollContent = document.getElementById("scroll-content");
const placeholder = document.querySelector(".placeholder");
const itemHeight = 50; // 每个项目的高度
const totalItems = 100; // 总项目数
const bufferItems = 5; // 缓冲项目数
const visibleItems = Math.ceil(scrollContainer.clientHeight / itemHeight); // 可见的项目数
const renderedItems = visibleItems + bufferItems; // 渲染的项目数
placeholder.style.height = `${totalItems * itemHeight}px`;//给placeholder设宽度为总项目数*高度
渲染函数部分
js
// 生成虚拟滚动内容
function renderItems(startIndex) { //startIndex为应该渲染的第一项的索引
startIndex = Math.max(0, Math.min(startIndex, totalItems - visibleItems));//边界检查 Math.min()是为了限制索引的不要超过最大项目数-可见项目数,Math.max防止索引小于0
const fragment = document.createDocumentFragment();
let i = startIndex;
for (i; i < startIndex + renderedItems; i++) { //开始渲染 起始为startIndex,到startIndex + renderedItems终止
if (i > totalItems - 1) break; //超过最大项数停止
const item = document.createElement("div");
item.className = "item";
item.style.height = `${itemHeight}px`;
item.style.lineHeight = `${itemHeight}px`;
item.style.boxSizing = "border-box";
item.style.padding = "0 10px";
item.style.borderBottom = "1px solid #eee";
item.textContent = `Item ${i + 1}`;
fragment.appendChild(item);
}
scrollContent.style.height = `${(i - startIndex) * itemHeight}px`;//重设可见区域高度为实际渲染了多少项的高度
scrollContent.innerHTML = "";
scrollContent.appendChild(fragment);//重新渲染可见区域
}
Math.min(startIndex, totalItems - visibleItems)
是为了限制索引的不要超过最大项目数-可见项目数,比如一共100项,可见10项,拉到最下面也就从90项开始渲染,不能再多了。 Math.max
是防止索引小于0,比如一共5项,可见项目10,如果只有Math.min就-5了,加个Math.max
防止其小于0。 重设scrollContent.style.height
是防止可见区域的高度大于剩余项的高度 比如还剩10项,renderedItems始终是15,如果不加的话高度就为15*50px了。
渲染部分
js
// 初始渲染
renderItems(0);
// 使用requestAnimationFrame优化滚动性能
let isScrolling = false;
scrollContainer.addEventListener("scroll", () => {
if (!isScrolling) {
window.requestAnimationFrame(() => {
const scrollTop = scrollContainer.scrollTop;//滚动了多少
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight));//重新计算应渲染的下一项的索引
renderItems(startIndex);
scrollContent.style.transform = `translateY(${startIndex * itemHeight}px)`;//将可视区域向下移动 每往上滚了一个itemHeight就往下移一个itemHeight
isScrolling = false;
});
isScrolling = true;
}
});
采用transform来实现滚动效果,原理大概如下 startIndex最开始是0 当scrollTop为50时意味着往上滚动了整整一项,那么这时该从第二项开始渲染了,startIndex变为1,同时translate 50px 让刚才滚出视口的那个渲染第一项的容器平移下来,用来渲染第二项。