开发虚拟滚动的不定高组件
开发的过程中我们只要处理一个问题即可。renderList,即渲染的数据列表
我们带着如何获取renderList这个问题去进行逻辑梳理
首先组件内部接收两个值,渲染的数据和每一项的高度
const {list, itemHeight} = defineProps({list: { // 渲染的数据type: Array,default: () => [],},itemHeight: { // 预估每一项的高度type: Number,default: 100,},
})
我们先去计算renderList(页面可视区域渲染的列表)
const renderList = computed(() => list.slice(startIndex.value, endIndex.value))
想要获取renderList需要知道页面可视区域的第一条数据和最后一条数据的下标,初始化的时候,startIndex 的值为0.,随着滚动更新startIndex,endIndex的值为startIndex+renderCount(可视区域的数量);所以我们的代码如下:
const renderCount = computed(() => Math.ceil(containerHeight.value/itemHeight))
const endIndex = computed(() => startIndex.value + renderCount.value)
其中的containerHeight为可视区域的高度:
containerHeight.value = containerRef.value.clientHeight || 0;
因为以上的renderList是我们根据预估的高度来进行计算的,我们要想得到真实的renderList,需要获取到真实的高度
获取startIndex我们需要根据列表的每项的真实高度来计算startIndex的值,我们定义一个变量来存储每项的下标(index)、top、bottom和height。
const position = ref([]);
function initPosition() {position.value = [];list.forEach((d, i) => {position.value.push({index: i,height: itemHeight,top: i * itemHeight,bottom: (i + 1) * itemHeight,});});
}
每次获取到list数据以后我们初始化position。
watch(() => list, () => {initPosition();
},{immediate: true
})
此时获取的都是最小高度,我们获取真实高度的时候要等页面上渲染以后才能获取到,所以我们要等页面更新完dom以后进行更新:
<template><div ref="containerRef" class="container" @scroll="handleScroll"><div class="container-list" :style="scrollStyle" ref="listRef"><div class="container-list-item" v-for="item in renderList" :key="item.index" :itemid="item.index">{{ item.index }}{{ item.content }}</div></div></div>
</template>
<script setup>
onUpdated(() => {updatePosition();
})
function updatePosition(){//获取listRef下的子元素const nodes = listRef.value ? listRef.value.children : [];if(!nodes?.length) return;const data = [...nodes];// 遍历所有的子元素更新真实的高度data.forEach(el => {let index = +el.getAttribute('itemid');const realHeight = el.getBoundingClientRect().height;// 判断默认的高度和真实的高度之差let diffVal = position.value[index].height - realHeight;if (diffVal !== 0) {for(let i = index; i < position.value.length; i++) {position.value[i].height = realHeight;position.value[i].top = position.value[i].top - diffVal;position.value[i].bottom = position.value[i].bottom - diffVal;}}})
}
</script>
代码中的itemid为完整数据的下标,保存下来更新position的的值的时候会用到。
获取到真实的高度以后我们就能计算startIndex了,如果item.bottom > scrollTop (滚动的高度)&& item.top <= scrollTop则,当前数据为可视区域的第一项,因为position中的bottom的值是递增的,我们只需要找到第一个bottom > scrollTop的值的下标即可,position.value.findIndex(item => item.bottom > scrollTop)。
使用二分法查找进行优化:
function handleScroll(e) {const scrollTop = e.target.scrollTop;startIndex.value = getStartIndex(scrollTop);
}
// 优化前
function getStartIndex(scrollTop) {return position.value.findIndex(item => item.bottom > scrollTop)
}
// 优化后
const getStartIndex = (scrollTop) => {let left = 0;let right = position.value.length - 1;while (left <= right) {const mid = Math.floor((left + right) / 2);if(position.value[mid].bottom == scrollTop) {return mid + 1;} else if (position.value[mid].bottom > scrollTop) {right = mid - 1;} else if (position.value[mid].bottom < scrollTop) {left = mid + 1;}}return left;
}
至此我们就获取到了我们需要的renderList,我们只需要给list容易写上样式即可,list的高度为:position的最后一项的bottom-滚动卷上去的高度,其中卷上去的高度为可视区域第一项的top值。
// 卷上去的高度
const scrollTop = computed(()=> startIndex.value > 0 ? position.value[startIndex.value-1].bottom : 0);
// list元素的整体高度
const listHeight = computed(() => position.value[position.value.length - 1].bottom);
const scrollStyle = computed(() => {return {height:`${listHeight.value - scrollTop.value}px`,transform: `translate3d(0, ${scrollTop.value}px, 0)`,}
})
完整代码:
父组件:
<template><div class="virtual-scroll"><Viru :list="list" :item-size="50"/></div>
</template>
<script setup>
import { getListData } from './data';
import { ref } from 'vue';
import Viru from './virtualUnfixedList.vue';
/*** list格式为:* [* {* index: 1,* conten: 'xxx'* },* {* index: 2,* content: 'xxx'* }* ]*/
const list = ref(getListData());
</script>
<style lang="scss" scoped>
.virtual-scroll {height: 500px; width: 500px;border: 1px solid red;
}</style>
子组件:
<template><div ref="containerRef" class="container" @scroll="handleScroll"><div class="container-list" :style="scrollStyle" ref="listRef"><div class="container-list-item" v-for="item in renderList" :key="item.index" :itemid="item.index">{{ item.index }}{{ item.content }}</div></div></div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated } from 'vue'const {list, itemHeight} = defineProps({list: { // 渲染的数据type: Array,default: () => [],},itemHeight: { // 预估每一项的高度type: Number,default: 100,},
})
const containerRef = ref(null);
const listRef = ref(null);
const startIndex = ref(0);
const containerHeight = ref(0);
const position = ref([]);const scrollTop = computed(()=> startIndex.value > 0 ? position.value[startIndex.value-1].bottom : 0);
const listHeight = computed(() => position.value[position.value.length - 1].bottom);const scrollStyle = computed(() => {return {height:`${listHeight.value - scrollTop.value}px`,transform: `translate3d(0, ${scrollTop.value}px, 0)`,}
})const renderCount = computed(() => Math.ceil(containerHeight.value/itemHeight))
const endIndex = computed(() => startIndex.value + renderCount.value)const renderList = computed(() => {return list.slice(startIndex.value, endIndex.value);
})onMounted(() => {containerHeight.value = containerRef.value.clientHeight || 0;
})
onUpdated(() => {updatePosition();
})watch(() => list, () => {initPosition();
},{immediate: true
})
function initPosition() {position.value = [];console.log(list);list.forEach((d, i) => {position.value.push({index: i,height: itemHeight,top: i * itemHeight,bottom: (i + 1) * itemHeight,});});
}
function updatePosition(){//获取listRef下的子元素const nodes = listRef.value ? listRef.value.children : [];if(!nodes?.length) return;const data = [...nodes];console.log(nodes)// 遍历所有的子元素更新真实的高度data.forEach(el => {let index = +el.getAttribute('itemid');const realHeight = el.getBoundingClientRect().height;// 判断默认的高度和真实的高度之差let diffVal = position.value[index].height - realHeight;if (diffVal !== 0) {for(let i = index; i < position.value.length; i++) {position.value[i].height = realHeight;position.value[i].top = position.value[i].top - diffVal;position.value[i].bottom = position.value[i].bottom - diffVal;}}})
}
function handleScroll(e) {const scrollTop = e.target.scrollTop;startIndex.value = getStartIndex(scrollTop);
}const getStartIndex = (scrollTop) => {let left = 0;let right = position.value.length - 1;while (left <= right) {const mid = Math.floor((left + right) / 2);if(position.value[mid].bottom == scrollTop) {return mid + 1;} else if (position.value[mid].bottom > scrollTop) {right = mid - 1;} else if (position.value[mid].bottom < scrollTop) {left = mid + 1;}}return left;
}</script>
<style scoped lang="scss">
.container {width: 100%;height: 100%;overflow: auto;&-list{width: 100%;&-item{width: 100%;}}
}
</style>