虚拟列表实现

  • 简介

    • 虚拟列表又称为长列表,当有些业务场景不适合用分页且数据量大时,就需要用到虚拟列表
    • 虚拟列表其实就是按需显示的一种技术,即对可见区域进行渲染。对不可见区域不渲染或者部分渲染
  • 列表项固定高度的虚拟列表

    • 实例图解

      1. 假设现在由10000条数据,可展示区域的高度是500px,列表项的高度是50px,则我们最多能看到10条数据项,即我们每次加载也只加载10条数据即可
        在这里插入图片描述

      2. 列表滚动时,可通过计算得出当前可视区可以展示的列表项。比如滚动高度scrollTop=150,那么现在列表展示的就是第4项到第13项
        在这里插入图片描述

    • 实现

      1. 获取列表数据 listData
      2. 获取列表项的高度 itemHeight
      3. 获取容器的高度 screenHeight
      4. 获取列表的高度 listHeight = listData.lenght * itemHeight
      5. 获取可显示的列表项数 visibleCount = Math.ceil(screenHeight / itemHeight)
      6. 获取索引的开始位置 startIndex = 0 || Math.floor(scrollTop / itemHeight)
      7. 获取索引的结束位置 endIndex = startIndex + visibleCount
      8. 获取可显示的列表数据 visibleData = listData.slice(startIndex, Math.min(endIndex, listData.lenght))
      9. 获取列表偏移量 startOffset = scrollTop - scrollTop % itemHeight

      代码实现:virtual-container的高度是由virtual-phantom来撑开,滚动的时候通过改变startIndexstartOffset 来调整可显示的列表数据visibleData 和virtual-list的位置调整

      <div
          ref="virtualListRef"
          class="virtual-container"
          :style="{ height: screenHeight + 'px' }"
          @scroll="scrollList"
        >
          <div class="virtual-phantom" :style="{ height: listHeight + 'px' }"></div>
          <div class="virtual-list" :style="{ transform: transform }">
            <div v-for="(item, index) in visibleData" class="item">
              {{ item.name }}
            </div>
          </div>
        </div>
      
      .virtual-container {
        width: 100%;
        height: 100%;
        border: 1px solid #58a;
        overflow: auto;
        position: relative;
        .virtual-phantom {
          position: absolute;
          left: 0;
          top: 0;
          right: 0;
          z-index: -1;
        }
        .virtual-list {
          position: absolute;
          left: 0;
          top: 0;
          right: 0;
          .item {
            border-bottom: 1px solid #58a;
            height: 40px;
            line-height: 40px;
          }
        }
      }
      
  • 列表项动态高度的虚拟列表

    • 每项高度不确定,以预估高度高度先进行渲染,再获取真实高度缓存。在onUpdated生命周期函数里面实时更新position
    • 实现
      1. 获取列表数据 listData

      2. 获取列表项的预估高度 estimatedItemSize

      3. 获取容器的高度 screenHeight

      4. 记录每项数据的位置 position

        // 初始时,以预估高度给position赋值
        position.value = props.virtualList.map((item, index: number) => {
            return {
                index,
                height: props.estimatedItemSize,
                top: index * props.estimatedItemSize,
                bottom: (index + 1) * props.estimatedItemSize,
            };
        });
        // 实时更新位置
        const updatePosition = () => {
           const nodes = itemRef.value!;
           nodes.forEach((node) => {
              const rect = node.getBoundingClientRect();
              const height = rect.height;
              const index = Number(node.id);
              const oldHeight = position.value[index].height;
              const diff = height - oldHeight;
              if (diff) {
                  position.value[index].height = height;
                  position.value[index].bottom = position.value[index].bottom + diff;
                  for (let k = index + 1; k < position.value.length; k++) {
                    position.value[k].top = position.value[k - 1].bottom;
                    position.value[k].bottom = position.value[k].bottom + diff;
                  }
              }
           });
        };
        
      5. 获取列表的高度 listHeight = position[position.lenght - 1].bottom

      6. 获取可显示的列表项数 visibleCount = Math.ceil(screenHeight / estimatedItemSize)

      7. 获取索引的开始位置 startIndex;

        // 普通查找
        const getStartIndex = (scrollTop = 0) => {
        let item = position.value.find((i) => i && i.bottom > scrollTop);
            return item.index;
         };
        
        // 二分查找快速
        const getStartIndex = (scrollTop: number) => {
        	let start = 0;
        	let end = position.value.length - 1;
        	let tempIndex = -1;
        	while (start <= end) {
        		let midIndex = parseInt(String((start + end) / 2));
        		let midValue = position.value[midIndex].bottom;
        	  if (midValue === scrollTop) {
        	      return midIndex + 1;
        	  } else if (midValue < scrollTop) {
        	      start = midIndex + 1;
        	  } else if (midValue > scrollTop) {
        	      if (tempIndex === -1 || tempIndex > midIndex) {
        	           tempIndex = midIndex;
        	      }
        	      end = midIndex - 1;
        	   }
        	 }
        	 return tempIndex;
        };
        
      8. 获取索引的结束位置 endIndex = startIndex + visibleCount

      9. 缓存容量 buffer =1

      10. 上方缓存数据量 aboveCount = Math.min(startIndex, buffer * visibleCount)

      11. 下方缓存数据量 belowCount = Math.min(endIndex, buffer * visibleCount)

      12. 获取可显示的列表数据 visibleData = listData.slice(startIndex - aboveCount endIndex + belowCount)

      13. 获取列表偏移量 startOffset

        // 获取偏移量
        const setStartOffset = () => {
            let startOffset = 0;
            // 含有上下缓存区
            if (startIndex.value < 1) {
                startOffset = 0;
             } else {
                const size =
                  position.value[startIndex.value].top -
                  (position.value[startIndex.value - aboveCount.value]
                    ? position.value[startIndex.value - aboveCount.value].top
                    : 0);
                startOffset = position.value[startIndex.value].top - size;
             }
             listRef.value!.style.transform = `translate3d(0,${startOffset}px,0)`;
        };
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值