 虚拟列表
          虚拟列表
        
 # 最简单的虚拟列表
https://wallpaper.shenzjd.com/#/vitualList/simple (opens new window)
<template>
  <div class="virtual" @scroll="onScroll">
    <div
      class="virtual-phantom"
      :style="{ height: `${data.length * itemHeight}px` }"></div>
    <div
      class="virtual-list"
      :style="{ transform: `translateY(${startTop}px)` }">
      <div
        v-for="item in virtualList"
        :key="item"
        class="item"
        :style="{ height: itemHeight + 'px' }">
        {{ item }}
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed } from "vue";
const data = ref(Array.from({ length: 100 }, (_, i) => i + 1));
const start = ref(0);
const count = ref(6);
const end = computed(() => start.value + count.value);
const itemHeight = 200;
const virtualList = computed(() => {
  return data.value.slice(start.value, end.value);
});
const startTop = ref(0);
const onScroll = (e) => {
  const scrollTop = e.target.scrollTop;
  start.value = Math.floor(scrollTop / itemHeight);
  // 向下取整(比较好理解)
  startTop.value =
    scrollTop % itemHeight
      ? Math.floor(scrollTop / itemHeight) * itemHeight
      : scrollTop;
  // 通用写法
  // startTop.value = scrollTop - (scrollTop % itemHeight);
};
</script>
<style lang="scss" scoped>
.virtual {
  height: 100%;
  overflow: auto;
  position: relative;
  .virtual-phantom {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
  }
  .virtual-list {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    .item {
      background-color: #eee;
      display: flex;
      justify-content: center;
      align-items: center;
      border-bottom: 1px solid #ccc;
      box-sizing: border-box;
    }
  }
}
</style>
# 带有上下缓存的虚拟列表
https://wallpaper.shenzjd.com/#/vitualList/buffer (opens new window)
<template>
  <div class="virtual-wapper" @scroll="onScroll">
    <div
      class="virtual-background"
      :style="{ height: totalHeight + 'px' }"></div>
    <div
      class="virtual-list"
      :style="{
        top: -(topBufferLength * itemHeight) + 'px',
        bottom: -(bottomBufferLength * itemHeight) + 'px',
        transform: `translate3d(0, ${startOffset}px, 0)`,
      }">
      <div
        v-for="item in virtualList"
        :key="item"
        class="item"
        :style="{ height: itemHeight + 'px' }">
        {{ item }}
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onBeforeMount } from "vue";
const data = ref(Array.from({ length: 100 }, (_, i) => i + 1));
const itemHeight = 100;
const totalHeight = computed(() => data.value.length * itemHeight);
// 起始索引
const startIndex = ref(0);
// 显示行数
const count = ref(0);
// 结束索引
const endIndex = ref(0);
// 缓冲行数
const buffer = ref(2);
// 缓冲起始索引
const bufferStartIndex = computed(() => {
  return Math.max(0, startIndex.value - buffer.value);
});
// 缓冲结束索引
const bufferEndIndex = computed(() => {
  return Math.min(data.value.length, endIndex.value + buffer.value);
});
// 顶部缓冲个数
const topBufferLength = computed(() => {
  return startIndex.value - Math.max(0, startIndex.value - buffer.value);
});
// 底部缓冲个数
const bottomBufferLength = computed(() => {
  return (
    Math.min(endIndex.value + buffer.value, data.value.length) - endIndex.value
  );
});
const virtualList = computed(() => {
  return data.value.slice(bufferStartIndex.value, bufferEndIndex.value);
});
onBeforeMount(() => {
  const { innerHeight } = window;
  count.value = Math.ceil(innerHeight / itemHeight);
  endIndex.value = startIndex.value + count.value;
});
const startOffset = ref(0);
const onScroll = (event) => {
  const scrollTop = event.target.scrollTop;
  startIndex.value = Math.floor(scrollTop / itemHeight);
  endIndex.value = Math.min(startIndex.value + count.value, data.value.length);
  startOffset.value = scrollTop - (scrollTop % itemHeight);
};
</script>
<style lang="scss" scoped>
.virtual-wapper {
  height: 100%;
  position: relative;
  overflow: auto;
  .virtual-background {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
  }
  .virtual-list {
    position: absolute;
    left: 0;
    right: 0;
    .item {
      background-color: #eee;
      display: flex;
      justify-content: center;
      align-items: center;
      border-bottom: 1px solid #ccc;
      box-sizing: border-box;
    }
  }
}
</style>
# 不定高度的虚拟列表
https://wallpaper.shenzjd.com/#/vitualList/variableHeight (opens new window)
<template>
  <div ref="wapper" class="virtual-wapper" @scroll="onScroll">
    <div
      class="virtual-background"
      :style="{ height: totalHeight + 'px' }"></div>
    <div
      class="virtual-list"
      :style="{
        transform: `translate3d(0, ${startOffset}px, 0)`,
      }">
      <div
        v-for="item in virtualList"
        :key="item"
        class="item"
        :style="{ height: item.height + 'px' }">
        第{{ item.id }}个,高度{{ item.height }}px
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
/**
 * Generates a random number between the given minimum and maximum values (inclusive).
 *
 * @param {number} min - The minimum value of the range.
 * @param {number} max - The maximum value of the range.
 * @return {number} - A random number between the given minimum and maximum values.
 */
const generateRandomNumber = (min, max) => {
  return Math.floor(Math.random() * (max - min + 1) + min);
};
const data = ref(
  Array.from({ length: 10 }, (_, i) => {
    return {
      id: i + 1,
      height: generateRandomNumber(100, 700),
    };
  })
);
// 预估高度, 用已知的高度来计算平均高度
const estimatedHeight = computed(() => {
  if (!cacheHeight.get(endIndex.value)) {
    return 100;
  }
  return cacheHeight.get(endIndex.value) / (endIndex.value + 1);
});
// 缓存的实际总高度
const cacheHeight = new Map();
// 起始索引
const startIndex = ref(0);
const wapper = ref(null);
// 结束索引
const endIndex = ref(1);
// 设置缓存
const setCacheHeight = (i) => {
  if (!cacheHeight.has(i)) {
    cacheHeight.set(
      i,
      i === 0
        ? data.value[i].height
        : cacheHeight.get(i - 1) + data.value[i].height
    );
  }
};
// 当前页面不够展示一页就要加载更多
const checkEndIndex = () => {
  while (
    cacheHeight.get(endIndex.value - 1) - cacheHeight.get(startIndex.value) <=
      wapper.value.offsetHeight &&
    endIndex.value < data.value.length - 1
  ) {
    endIndex.value++;
    setCacheHeight(endIndex.value);
  }
  while (
    cacheHeight.get(endIndex.value - 1) - cacheHeight.get(startIndex.value) >
    wapper.value.offsetHeight
  ) {
    endIndex.value--;
  }
};
onMounted(() => {
  for (let i = 0; i <= endIndex.value; i++) {
    setCacheHeight(i);
  }
  checkEndIndex();
});
const virtualList = computed(() => {
  // 计算显示行数
  return data.value.slice(startIndex.value, endIndex.value + 1);
});
const totalHeight = computed(() => {
  return (
    (data.value.length - 1 - endIndex.value) * estimatedHeight.value +
    cacheHeight.get(endIndex.value)
  );
});
const startOffset = ref(0);
const onScroll = (event) => {
  const { scrollTop } = event.target;
  // 先判断是否在最顶部的上面还是下面
  if (scrollTop > cacheHeight.get(startIndex.value)) {
    let i = 0;
    while (scrollTop > cacheHeight.get(startIndex.value + i)) {
      i++;
    }
    startIndex.value = startIndex.value + i;
    startOffset.value = scrollTop;
  }
  if (
    startIndex.value > 0 &&
    scrollTop < cacheHeight.get(startIndex.value - 1)
  ) {
    let i = 0;
    while (scrollTop < cacheHeight.get(startIndex.value - 1 - i)) {
      i++;
    }
    startIndex.value = startIndex.value - i;
    startOffset.value = scrollTop - data.value[startIndex.value].height;
  }
  console.log(`在索引为${startIndex.value}的元素上`);
  checkEndIndex();
};
</script>
<style lang="scss" scoped>
.virtual-wapper {
  height: 100%;
  position: relative;
  overflow: auto;
  .virtual-background {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
  }
  .virtual-list {
    position: absolute;
    left: 0;
    right: 0;
    .item {
      background-color: #eee;
      display: flex;
      justify-content: center;
      align-items: center;
      border-bottom: 1px solid #ccc;
      box-sizing: border-box;
    }
  }
}
</style>
编辑  (opens new window)
  上次更新: 2025/10/09, 07:47:31
