关键词:虚拟滚动、虚拟加载
原理
虚拟滚动(Virtual Scrolling)是一种性能优化的手段,通常用于处理长列表的显示问题。在传统的滚动加载中,当面对成千上万项的长列表时,直接在 DOM 中创建并展示所有项会导致严重的性能问题,因为浏览器需要渲染所有的列表项。而虚拟滚动的核心原理是仅渲染用户可视范围内的列表项,以此减少 DOM 操作的数量和提高性能。
实现虚拟滚动,我们需要:
- 监听滚动事件,了解当前滚动位置。
- 根据滚动位置计算当前应该渲染哪些列表项目(即在视口内的项目)。
- 只渲染那些项目,并用占位符(比如一个空的 div)占据其它项目应有的位置,保持滚动条大小不变。
- 当用户滚动时,重新计算并渲染新的项目。
基础版本实现
以下是一个简单的虚拟滚动实现的 JavaScript 代码示例:
class VirtualScroll {
constructor(container, itemHeight, totalItems, renderCallback) {
this.container = container; // 容器元素
this.itemHeight = itemHeight; // 每个项的高度
this.totalItems = totalItems; // 总列表项数
this.renderCallback = renderCallback; // 渲染每一项的回调函数
this.viewportHeight = container.clientHeight; // 视口高度
this.bufferSize = Math.ceil(this.viewportHeight / itemHeight) * 3; // 缓冲大小
this.renderedItems = []; // 已渲染项的数组
this.startIndex = 0; // 当前渲染的开始索引
this.endIndex = this.bufferSize; // 当前渲染的结束索引
container.addEventListener("scroll", () => this.onScroll());
this.update();
}
onScroll() {
const scrollTop = this.container.scrollTop;
const newStartIndex = Math.floor(scrollTop / this.itemHeight) - this.bufferSize / 2;
const newEndIndex = newStartIndex + this.bufferSize;
if (newStartIndex !== this.startIndex || newEndIndex !== this.endIndex) {
this.startIndex = Math.max(0, newStartIndex);
this.endIndex = Math.min(this.totalItems, newEndIndex);
this.update();
}
}
update() {
// 清空已有内容
this.container.innerHTML = "";
// 计算并设置容器的总高度
const totalHeight = this.totalItems * this.itemHeight;
this.container.style.height = `${totalHeight}px`;
// 渲染视口内的项
const fragment = document.createDocumentFragment();
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderCallback(i);
item.style.top = `${i * this.itemHeight}px`;
fragment.appendChild(item);
}
this.container.appendChild(fragment);
}
}
// 创建一个列表项的函数
function createItem(index) {
const item = document.createElement("div");
item.className = "list-item";
item.innerText = `Item ${index}`;
item.style.position = "absolute";
item.style.width = "100%";
return item;
}
// 初始化虚拟滚动
const container = document.querySelector(".scroll-container"); // 容器元素需要预先在HTML中定义
const virtualScroll = new VirtualScroll(container, 30, 10000, createItem);
这个例子中,我们创建了一个VirtualScroll
类,通过传入容器、项高度、总项数和渲染回调函数来进行初始化。该类的update
方法用于渲染出当前可视范围内部分的项目,并将它们放到文档碎片中,然后一次性添加到容器中。这样可以避免多次直接操作 DOM,减少性能消耗。当滚动时,onScroll
方法将计算新的startIndex
和endIndex
,然后调用update
方法进行更新。请注意,实际应用可能需要根据具体情况调整缓冲区大小等参数。
进阶版本:使用 IntersectionObserver 来实现
使用 IntersectionObserver
实现虚拟滚动就意味着我们会依赖于浏览器的 API 来观察哪些元素进入或离开视口(viewport),而非直接监听滚动事件。这样我们只需在需要时渲染或回收元素。
以下是一个简化版使用 IntersectionObserver
来实现虚拟滚动的例子:
class VirtualScroll {
constructor(container, itemHeight, totalItems, renderItem) {
this.container = container;
this.itemHeight = itemHeight;
this.totalItems = totalItems;
this.renderItem = renderItem;
this.observer = new IntersectionObserver(this.onIntersection.bind(this), {
root: this.container,
threshold: 1.0,
});
this.items = new Map();
this.init();
}
init() {
// 填充初始屏幕的元素
for (let i = 0; i < this.totalItems; i++) {
const placeholder = this.createPlaceholder(i);
this.container.appendChild(placeholder);
this.observer.observe(placeholder);
}
}
createPlaceholder(index) {
const placeholder = document.createElement("div");
placeholder.style.height = `${this.itemHeight}px`;
placeholder.style.width = "100%";
placeholder.dataset.index = index; // store index
return placeholder;
}
onIntersection(entries) {
entries.forEach((entry) => {
const index = entry.target.dataset.index;
if (entry.isIntersecting) {
const rendered = this.renderItem(index);
this.container.replaceChild(rendered, entry.target);
this.items.set(index, rendered);
} else if (this.items.has(index)) {
const placeholder = this.createPlaceholder(index);
this.container.replaceChild(placeholder, this.items.get(index));
this.observer.observe(placeholder);
this.items.delete(index);
}
});
}
}
// Render item function
function renderItem(index) {
const item = document.createElement("div");
item.classList.add("item");
item.textContent = `Item ${index}`;
item.dataset.index = index;
item.style.height = "30px"; // Same as your itemHeight in VirtualScroll
return item;
}
// Example usage:
const container = document.getElementById("scroll-container"); // This should be a predefined element in your HTML
const itemHeight = 30; // Height of each item
const itemCount = 1000; // Total number of items you have
const virtualScroll = new VirtualScroll(container, itemHeight, itemCount, renderItem);
在这里我们创建了一个 VirtualScroll
类,构造函数接收容器元素、每个项的高度、总项目数和用于渲染每个项目的函数。我们在初始化方法中,为每个项目创建了一个占位符元素,并且向 IntersectionObserver
注册了这些占位元素。
当一个占位元素进入到视口中时,我们就会渲染对应的项,并且将它替换这个占位符。当一个项离开视口,我们又会将它替换回原来的占位符并取消它的注册。
这种方法的优势包括:
- 不需要绑定滚动事件,防止滚动性能问题。
- 浏览器会自动优化观察者的回调。
- 不需要手动计算当前应该渲染的项目,当用户快速滚动时也不会遇到空白内容。