<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">
  <div class="home">
    <div class="title">虚拟列表:仅可见区域渲染元素</div>
    <div class="wrapper" ref="wrapperRef" @scroll.passive="onWrapperScroll">
      <div class="filler" :style="{ height: `${contentHeight}px` }"></div>
      <div class="list" :style="{ transform: `translate3d(0, ${startOffset}px, 0)` }">
        <div class="list-item" v-for="(item, index) in visibleData" :key="index">第{{ item }} 条数据</div>
      </div>
    </div>
  </div>
</div>
.title {
  font-size: 24px;
}
.wrapper {
  margin-top: 20px;
  width: 100%;
  height: 400px;
  background-color: rgb(245, 218, 224);
  position: relative;
  overflow: auto;
  .filler {
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    z-index: -1;
  }
  .list {
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    color: #000;
    .list-item {
      height: 30px;
      line-height: 30px;
      border-bottom: 1px solid #000;
    }
  }
}
View Compiled
const { createApp, ref, onMounted, computed } = Vue;
createApp({
  setup() {
    // 数据源
    const list = [];
    for (let index = 0; index < 1000; index++) {
      list.push(index);
    }

    // 列表项元素高度
    const itemHeight = 30;
    // 滚动容器高度
    const contentHeight = computed(() => list.length * itemHeight);

    /**
     * 核心思路:
     * 根据容器滚动的高度,动态设置可见区域(元素)距离容器的偏移量;
     * 同时根据滚动高度可获取可见区域内起始元素的索引,根据起始元素的位置始终可以得到可见区域内末尾元素的索引
     *   由首尾两个元素索引可以从数据源里获取到对应区间的需要渲染的元素
     */
    const startIndex = ref(0);
    const endIndex = ref(0);
    // 缓冲区元素:额外渲染的元素,使滚动时能更平滑的切换元素渲染
    const bufferCount = 4;
    const visibleData = ref([]);
    // 可见区域的偏移量:跟随容器滚动时动态更新
    const startOffset = ref(0);
    const updateVisibleData = (scrollTop = 0) => {
      startIndex.value = Math.floor(scrollTop / itemHeight);
      // 可见元素的数量:可见区域的高度 / 列表项元素的高度
      const visibleCount =
        Math.ceil(document.querySelector('.wrapper').clientHeight / 30) + bufferCount;
      // 末尾元素索引:起始元素 + 可见区域总共可渲染的元素数量
      endIndex.value = startIndex.value + visibleCount;
      visibleData.value = list.slice(startIndex.value, endIndex.value);
      startOffset.value = startIndex.value * itemHeight;
    };

    const onWrapperScroll = (e) => {
      const scrollTop = e.target.scrollTop;
      updateVisibleData(scrollTop);
    };

    onMounted(() => {
      updateVisibleData();
    });

    return {
      onWrapperScroll,
      contentHeight,
      startOffset,
      visibleData
    };
  }
}).mount("#app");

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.