马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
开辟虚拟滚动的不定高组件
开辟的过程中我们只要处理一个题目即可。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>
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |