虚拟列表实现懒加载

1,830 阅读2分钟

一、适用场景:

1、返回列表数据量大,需要使用滚动条滚动实现动态渲染列表数据

2、返回数据是分页,但是需要通过滚动条实现分页渲染

二、实现方案:

1、原生JavaScript实现

2、Vue3.2实现

以下两个方案亲自验证过,都是一行一行代码敲出来的,非简单CTRL+C+V,描述力求简洁,如有疑问可关注留言

三、方案一:原生JavaScript实现

直接上JavaScript插件代码 virtual-list.js:

/**
 *by 郭太东
 *只需要传入三个参数,以及重载一个方法setItemElement用来创建自定义的列表元素
 */
class VirtualList {
  constructor(options) {
    this.options = Object.assign(
      {
        boxEle: null, // 列表外层的容器DOM元素对象 必选
        scrollEle: null, // 列表的容器DOM元素对象 必选
        data: [], // 数组列表数据 必选
      },
      options
    );
    // 单项占用高度
    this.itemH = 0;
    this.boxEle = this.options.boxEle;
    this.scrollEle = this.options.scrollEle;
    this.data = this.options.data;
    // div可视窗口最多可显示的项数
    this.maxVisibleNum = 0;
    // 当前已加载项数
    this.curItemNum = 0;
    // 为了更平滑的滚动
    this.isScrollBusy = false;
    this._this = this;
  }
  /**
   * 先渲染一个元素用来拿到每一项的实际数据
   */
  getEleInfo() {
    const fragment = document.createDocumentFragment();
    fragment.appendChild(this.setItemElement(0));
    this.scrollEle.append(fragment);
    this.itemH =
      this.scrollEle.children[0].clientHeight +
      Number(
        getComputedStyle(this.scrollEle.children[0]).marginBottom.replace(
          "px",
          ""
        )
      );
    this.maxVisibleNum = Math.floor(this.boxEle.clientHeight / this.itemH);
    this.curItemNum = this.maxVisibleNum * 2;
  }

  start() {
    if (this.data.length === 0) return;
    this.getEleInfo();
    // 初始化加载显示 (首次加载,分两种情况)
    const fragment = document.createDocumentFragment();
    // 第一种:列表总长度小于 this.maxVisibleNum*2
    let initNum = this.data.length;
    if (this.maxVisibleNum * 2 <= initNum) {
      // 第二种:可加载长度大于等于列表的总长度
      initNum = this.maxVisibleNum * 2;
      // 注册滚动事件
      this.boxEle.addEventListener("scroll", this.scrollHandler(this));
    }
    for (let i = 1; i < initNum; i++) {
      fragment.appendChild(this.setItemElement(i));
    }
    this.scrollEle.append(fragment);
    
  }

  scrollHandler(_this) {
    return () => {
      // 滚动事件发生,动态加载列表
      if (_this.isScrollBusy) return;
      _this.isScrollBusy = true;
      // requestAnimationFrame为了更平滑的滚动, 其实用setTimeout也可以
      window.requestAnimationFrame(() => {
        _this.isScrollBusy = false;
        // 滚动条件:滚动到最新加载列表的三分之二处,开始加载下一页列表
        const scrollLine =
          _this.scrollEle.clientHeight -
          _this.boxEle.clientHeight -
          Math.floor(_this.itemH / 3);
        const offsetTop = _this.boxEle.scrollTop; // 滚动X轴的偏移量
        if (offsetTop >= scrollLine) {
          // 开始加载的索引
          let sNum = _this.curItemNum;
          // 处理列表最后一页的数据
          if (_this.data.length > _this.curItemNum + _this.maxVisibleNum) {
            _this.curItemNum += _this.maxVisibleNum;
          } else {
            _this.curItemNum = _this.data.length;
          }
          const fragment = document.createDocumentFragment();
          for (let i = sNum; i < _this.curItemNum; i++) {
            fragment.appendChild(_this.setItemElement(i));
          }
          _this.scrollEle.append(fragment);
        }
      });
    };
  }
  setItemElement() {
    // 创建自定义的DOM元素
  }
}

实际使用参考Demo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>虚拟列表</title>
  </head>
  <style>
    .virtual-list-box {
      width: 300px;
      height: 300px;
      overflow: auto;
    }
    .virtual-list-box-item {
      margin: 0px 10px 20px 10px;
    }
  </style>
  <body>
    <div id="virtual-list-box" class="virtual-list-box">
      <div id="virtual-list-scroll" class="virtual-list-scroll"></div>
    </div>
  </body>
  <script src="./virtual-list.js"></script>
  <script>
    const _json = [];
    // 生成测试数据
    for (let i = 0; i < 100; i++) {
      _json.push({
        name: "虚拟列表--" + (i + 1),
      });
    }


    // 创建自定义的列表项DOM节点,重载setItemElement方法
    VirtualList.prototype.setItemElement = function (i) {
      let listNode = document.createElement("div");
      listNode.innerHTML = this.data[i].name;
      listNode.className = "virtual-list-box-item";
      return listNode;
    };
    // 传参初始化
    new VirtualList({
      boxEle: document.querySelector("#virtual-list-box"), // 列表外层的容器DOM元素对象 必选
      scrollEle: document.querySelector("#virtual-list-scroll"), // 列表的容器DOM元素对象 必选
      data: _json, // 数组列表数据 必选
    }).start();
  </script>
</html>

四、方案二:Vue3.2实现

虚拟列表组件 VirtualList.vue:

  <div ref="boxEle" class="virtual-list-box">
    <div ref="scrollEle" class="virtual-list-scroll">
      <div v-for="(v, i) in curData" :key="i" class="virtual-list-box-item">
        <slot :item="v"></slot>
      </div>
    </div>
  </div>
</template>
<script setup>
import { onMounted, ref, toRefs } from "vue";

const props = defineProps({
  // 列表数据
  data: {
    type: Array,
    default: () => [],
  },
  // 每行占用高度
  itemH: {
    type: Number,
    default: () => 0,
  },
});

const { data, itemH } = toRefs(props);

const boxEle = ref(null); //列表外层的容器DOM元素对象
const scrollEle = ref(null); // 列表的容器DOM元素对象
// div可视窗口最多可显示的项数
let maxVisibleNum = 0;
// 当前已加载项数
let curItemNum = 0;

const curData = ref([]);
// 为了更平滑的滚动
let isScrollBusy = false;

onMounted(() => {
  start();
});
function start() {
  if (data.value.length === 0) return;
  maxVisibleNum = Math.floor(boxEle.value.clientHeight / itemH.value);
  curItemNum = maxVisibleNum * 2;
  // 初始化加载显示 (首次加载,分两种情况)
  // 第一种:列表总长度小于 maxVisibleNum*2
  let initNum = data.value.length;
  if (maxVisibleNum * 2 <= initNum) {
    // 第二种:可加载长度大于等于列表的总长度
    initNum = maxVisibleNum * 2;
    // 注册滚动事件
    boxEle.value.addEventListener("scroll", scrollHandler());
  }
  curData.value = data.value.slice(0, initNum);
}

function scrollHandler() {
  return () => {
    // 滚动事件发生,动态加载列表
    if (isScrollBusy) return;
    isScrollBusy = true;
    // requestAnimationFrame为了更平滑的滚动, 其实用setTimeout也可以
    window.requestAnimationFrame(() => {
      isScrollBusy = false;
      // 滚动条件:滚动到最新加载列表的三分之二处,开始加载下一页列表
      const scrollLine =
        scrollEle.value.clientHeight -
        boxEle.value.clientHeight -
        Math.floor(itemH.value / 3);
      const offsetTop = boxEle.value.scrollTop; // 滚动X轴的偏移量
      if (offsetTop >= scrollLine) {
        // 处理列表最后一页的数据
        if (data.value.length > curItemNum + maxVisibleNum) {
          curItemNum += maxVisibleNum;
        } else {
          curItemNum = data.length;
        }
        curData.value = data.value.slice(0, curItemNum);
      }
    });
  };
}
</script>

<style lang="less" scoped>
.virtual-list-box {
  width: 300px;
  height: 300px;
  overflow: auto;
}
.virtual-list-box-item {
  margin: 0px 10px 20px 10px;
}
</style>

实际使用参考DEMO

<template>
  <VirtualList :itemH="40" :data="_json">
      <template v-slot="{ item }">
          <!-- 此处是列表呈现 -->
          <div>{{ item.name }}</div>
      </template>
  </VirtualList>
</template>
<script setup>
import { ref } from "vue";
import VirtualList from "./VirtualList.vue";

// 生成测试数据
const _json = ref([]);
for (let i = 0; i < 100; i++) {
  _json.value.push({
    name: "虚拟列表--" + (i + 1),
  });
}
</script>

<style lang="less" scoped></style>

以上是Vue的最新写法,[参见](单文件组件 )