虚拟列表
# 最简单的虚拟列表
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)
上次更新: 2024/11/29, 10:10:04