vue3中开辟一个不定高的虚拟滚动组件

打印 上一主题 下一主题

主题 720|帖子 720|积分 2160

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
开辟虚拟滚动的不定高组件
开辟的过程中我们只要处理一个题目即可。renderList,即渲染的数据列表
我们带着怎样获取renderList这个题目去进行逻辑梳理
首先组件内部接收两个值,渲染的数据和每一项的高度
  1. const {list, itemHeight} = defineProps({
  2.   list: { // 渲染的数据
  3.     type: Array,
  4.     default: () => [],
  5.   },
  6.   itemHeight: { // 预估每一项的高度
  7.     type: Number,
  8.     default: 100,
  9.   },
  10. })
复制代码
我们先去计算renderList(页面可视区域渲染的列表)
  1. const renderList = computed(() => list.slice(startIndex.value, endIndex.value))
复制代码
想要获取renderList需要知道页面可视区域的第一条数据和末了一条数据的下标,初始化的时间,startIndex 的值为0.,随着滚动更新startIndex,endIndex的值为startIndex+renderCount(可视区域的数量);以是我们的代码如下:
  1. const renderCount = computed(() => Math.ceil(containerHeight.value/itemHeight))
  2. const endIndex = computed(() => startIndex.value + renderCount.value)
复制代码
此中的containerHeight为可视区域的高度:
  1. containerHeight.value = containerRef.value.clientHeight || 0;
复制代码
因为以上的renderList是我们根据预估的高度来进行计算的,我们要想得到真实的renderList,需要获取到真实的高度
获取startIndex我们需要根据列表的每项的真实高度来计算startIndex的值,我们定义一个变量来存储每项的下标(index)、top、bottom和height。
  1. const position = ref([]);
  2. function initPosition() {
  3.         position.value = [];
  4.         list.forEach((d, i) => {
  5.                 position.value.push({
  6.                         index: i,
  7.                         height: itemHeight,
  8.                         top: i * itemHeight,
  9.                         bottom: (i + 1) * itemHeight,
  10.                 });
  11.         });
  12. }
复制代码
每次获取到list数据以后我们初始化position。
  1. watch(() => list, () => {
  2.         initPosition();
  3. },{
  4.         immediate: true
  5. })
复制代码
此时获取的都是最小高度,我们获取真实高度的时间要等页面上渲染以后才气获取到,以是我们要等页面更新完dom以后进行更新:
  1. <template>
  2.         <div ref="containerRef" class="container" @scroll="handleScroll">
  3.                 <div class="container-list" :style="scrollStyle" ref="listRef">
  4.                         <div class="container-list-item" v-for="item in renderList" :key="item.index" :itemid="item.index">
  5.                                 {{ item.index }}{{ item.content  }}
  6.                         </div>
  7.                 </div>
  8.         </div>
  9. </template>
  10. <script setup>
  11. onUpdated(() => {
  12.         updatePosition();
  13. })
  14. function updatePosition(){
  15.         //获取listRef下的子元素
  16.   const nodes = listRef.value ? listRef.value.children : [];
  17.         if(!nodes?.length) return;
  18.         const data = [...nodes];
  19.         // 遍历所有的子元素更新真实的高度
  20.         data.forEach(el => {
  21.                 let index = +el.getAttribute('itemid');
  22.                 const realHeight = el.getBoundingClientRect().height;
  23.                 // 判断默认的高度和真实的高度之差
  24.                 let diffVal = position.value[index].height - realHeight;
  25.                 if (diffVal !== 0) {
  26.                         for(let i = index; i < position.value.length; i++) {
  27.                                 position.value[i].height = realHeight;
  28.                                 position.value[i].top = position.value[i].top - diffVal;
  29.                                 position.value[i].bottom = position.value[i].bottom - diffVal;
  30.                         }
  31.                 }
  32.         })
  33. }
  34. </script>
复制代码
代码中的itemid为完整数据的下标,生存下来更新position的的值的时间会用到。
获取到真实的高度以后我们就能计算startIndex了,如果item.bottom > scrollTop (滚动的高度)&& item.top <= scrollTop则,当前数据为可视区域的第一项,因为position中的bottom的值是递增的,我们只需要找到第一个bottom > scrollTop的值的下标即可,position.value.findIndex(item => item.bottom > scrollTop)。
使用二分法查找进行优化:
  1. function handleScroll(e) {
  2.         const scrollTop = e.target.scrollTop;
  3.         startIndex.value = getStartIndex(scrollTop);
  4. }
  5. // 优化前
  6. function getStartIndex(scrollTop) {
  7.         return position.value.findIndex(item => item.bottom > scrollTop)
  8. }
  9. // 优化后
  10. const getStartIndex = (scrollTop) => {
  11.         let left = 0;
  12.         let right = position.value.length - 1;
  13.         while (left <= right) {
  14.                 const mid = Math.floor((left + right) / 2);
  15.                 if(position.value[mid].bottom == scrollTop) {
  16.                         return mid + 1;
  17.                 } else if (position.value[mid].bottom > scrollTop) {
  18.                         right = mid - 1;
  19.                 } else if (position.value[mid].bottom < scrollTop) {
  20.                         left = mid + 1;
  21.                 }
  22.         }
  23.         return left;
  24. }
复制代码
至此我们就获取到了我们需要的renderList,我们只需要给list容易写上样式即可,list的高度为:position的末了一项的bottom-滚动卷上去的高度,此中卷上去的高度为可视区域第一项的top值。
  1. // 卷上去的高度
  2. const scrollTop = computed(()=> startIndex.value > 0 ? position.value[startIndex.value-1].bottom : 0);
  3. // list元素的整体高度
  4. const listHeight = computed(() => position.value[position.value.length - 1].bottom);
  5. const scrollStyle = computed(() => {
  6.         return {
  7.                 height:`${listHeight.value - scrollTop.value}px`,
  8.                 transform: `translate3d(0, ${scrollTop.value}px, 0)`,
  9.         }
  10. })
复制代码
完整代码:
父组件:
  1. <template>
  2.         <div class="virtual-scroll">
  3.                 <Viru :list="list" :item-size="50"/>
  4.         </div>
  5. </template>
  6. <script setup>
  7. import { getListData } from './data';
  8. import { ref } from 'vue';
  9. import Viru from './virtualUnfixedList.vue';
  10. /**
  11. * list格式为:
  12. * [
  13. *   {
  14. *      index: 1,
  15. *      conten: 'xxx'
  16. *   },
  17. *   {
  18. *      index: 2,
  19. *      content: 'xxx'
  20. *   }
  21. * ]
  22. */
  23. const list = ref(getListData());
  24. </script>
  25. <style lang="scss" scoped>
  26. .virtual-scroll {
  27.         height: 500px;
  28.         width: 500px;
  29.         border: 1px solid red;
  30. }
  31. </style>
复制代码
子组件:
  1. <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({
  2.   list: { // 渲染的数据
  3.     type: Array,
  4.     default: () => [],
  5.   },
  6.   itemHeight: { // 预估每一项的高度
  7.     type: Number,
  8.     default: 100,
  9.   },
  10. })
  11. 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))
  12. const endIndex = computed(() => startIndex.value + renderCount.value)
  13. const renderList = computed(() => {        return list.slice(startIndex.value, endIndex.value);})onMounted(() => {        containerHeight.value = containerRef.value.clientHeight || 0;
  14. })onUpdated(() => {        updatePosition();})watch(() => list, () => {
  15.         initPosition();
  16. },{
  17.         immediate: true
  18. })
  19. 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企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

诗林

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表