滚动混动加载原理是什么, 用 JS 代码简单实现一个虚拟滚动加加载。【热度: 354】

关键词:虚拟滚动、虚拟加载

原理

虚拟滚动(Virtual Scrolling)是一种性能优化的手段,通常用于处理长列表的显示问题。在传统的滚动加载中,当面对成千上万项的长列表时,直接在 DOM 中创建并展示所有项会导致严重的性能问题,因为浏览器需要渲染所有的列表项。而虚拟滚动的核心原理是仅渲染用户可视范围内的列表项,以此减少 DOM 操作的数量和提高性能。

实现虚拟滚动,我们需要:

  1. 监听滚动事件,了解当前滚动位置。
  2. 根据滚动位置计算当前应该渲染哪些列表项目(即在视口内的项目)。
  3. 只渲染那些项目,并用占位符(比如一个空的 div)占据其它项目应有的位置,保持滚动条大小不变。
  4. 当用户滚动时,重新计算并渲染新的项目。

基础版本实现

以下是一个简单的虚拟滚动实现的 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方法将计算新的startIndexendIndex,然后调用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 注册了这些占位元素。

当一个占位元素进入到视口中时,我们就会渲染对应的项,并且将它替换这个占位符。当一个项离开视口,我们又会将它替换回原来的占位符并取消它的注册。

这种方法的优势包括:

  • 不需要绑定滚动事件,防止滚动性能问题。
  • 浏览器会自动优化观察者的回调。
  • 不需要手动计算当前应该渲染的项目,当用户快速滚动时也不会遇到空白内容。