代码拆解,不定高虚拟滚动(虚拟列表)

        承接上文,定高虚拟滚动,继续展开。定高虚拟滚动http://t.csdn.cn/ZSXak

        不定高虚拟滚动,顾名思义。每一项的高度不是固定的,是根据实际内容撑开高度不一致的列表项。

        所以,想要实现不定高虚拟列表,不同于定高虚拟列表 ,需要:

                1、每一项的高度

                2、每一项距顶部的高度

                3、滚动的列表的总高度

        因为上述都不是提前定义的,需要先遍历数组循环项,给每一项添加一个大概的虚拟高度,这样可以将列表项撑开。

        在dom实际元素加载后,便可以获取到实际的每一项高度、距顶部高度、列表总高度,这时再将值重新赋回去。

        父元素设置 overflow:auto 后 ,列表项滚动,触发@scroll事件,可以获取到不断变化的scrollTop值,因为提前加载项数据是有限的,所以这时需要不断截取数据,并渲染回列表。

数据.slice(startIndex,endIndex)。

        根据获取到的动态scrollTop 二分法处理,同虚拟列表项距顶部高度不断作比较arr[mid] 为虚拟列高度的中间项,不断while 循环,利用二分之一将数组分割,减小搜索范围,直到最终定位到 目标 坐标 值,即为 startIndex 起始坐标值

        endIndex为 起始坐标值 startIndex + 页面渲染列表量。this.scrollTop重新定位到数组的startIndex位置。

        在计算属性中定义列表要循环的数据项,通过对原始数组不断的slice(startIndex,endIndex)进行截取,return获得的即为列表渲染项

<template>
  <div id="app">
    <input type="text" v-model.number="dataLength">条 Height:{{scrollBarHeight}}
      <div class="phantom" ref="scrollsss" @scroll="onScroll">
      <!-- 列表总高 -->
        <ul :style="{height: scrollBarHeight + 'px'}">
      <!-- 列表偏移量 -->
          <Item :style="{'transform': `translate3d(0,${scrollTop}px,0)`}" v-for="item in visibleList" :item="item" :index="item.index" :key="item.brandId" @update-height="updateItemHeight"/>
        </ul>
      </div>
    </div>
</template>
 
<script>
import Item from './Item.vue';
export default {
  name: "App",
  components: {
    Item
  },
  data() {
    return {
      estimatedItemHeight: 30, // 每一项假定的高度
      visibleCount: 10, // 可视窗口展示项
      dataLength: 100, // 模拟列表项
      startIndex: 0, // 截取数组的 起始 索引
      endIndex: 10, // 截取数组的 结束 索引
      scrollTop: 0, // 距离顶部的偏移量
      scrollBarHeight: 0, // 虚拟列表高度
      bufferItemCount: 4,  // 缓冲加载项
      dataList: [], // 数据列表
      itemHeightCache: [], // 每一项高度
      itemTopCache: [] // 每一项距顶部的实际高度
    };
  },
  computed: {
    // 截取要展示的数据
    visibleList() {
      return this.dataList.slice(this.startIndex, this.endIndex + this.bufferItemCount);
    }
  },
  watch: {},
  created() {
    // 初始化demo的列表数据项
    this.dataList = this.getDataList();
    this.generateEstimatedItemData();
  },
  mounted() {},
  methods: {
    generateEstimatedItemData() {
      const estimatedTotalHeight = this.dataList.reduce((pre, current, index)=> {
        // 给每一项一个虚拟高度
        this.itemHeightCache[index] = this.estimatedItemHeight;
        // 给每一项距顶部的虚拟高度
        this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight;
        return pre + this.estimatedItemHeight;
      }, 0);
      // 列表总高
      this.scrollBarHeight = estimatedTotalHeight;
    },
    updateItemHeight({index, height}) {
      // dom元素加载后得到实际高度 重新赋值回去
        this.itemHeightCache[index] = height;
         // 重新确定列表的实际总高度
        this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
          return pre + current;
        }, 0)
        let newItemTopCache = [0];
        for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
          // 虚拟每项距顶部高度 + 实际每项高度
          newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1]
        };
        // 获得每一项距顶部的实际高度
        this.itemTopCache = newItemTopCache;
    },
    getDataList() {
		console.log(this.dataLength);
    // 100展开为数组后为每一条数据添加属性
      const newDataList = [...Array(this.dataLength || 0).keys()].map((v, i) => ({
        index: i,
        brandId: i + 1,
        name: `第${i + 1}项`,
        height: Math.floor(Math.max(Math.random() * 10, 5)) * 10
      }));
	  console.log(newDataList);
      return newDataList;
    },
    // 获取渲染项起始索引
    getStartIndex(scrollTop) {
      // 每一项距顶部的距离
      let arr = this.itemTopCache;
      let index = -1;
      let left = 0,
        right = arr.length - 1,
        mid = Math.floor((left + right) / 2);
        // 判断 有可循环项时进入
      while (right - left > 1) {
      /*
        二分法:拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
        arr[mid] 为虚拟列高度的中间项 
        不断while 循环,利用二分之一将数组分割,减小搜索范围
        直到最终定位到 目标index 值
      */
        // 目标数在左侧
        if (scrollTop < arr[mid]) {
          right = mid;
          mid = Math.floor((left + right) / 2);
        } else if (scrollTop > arr[mid]) {
          // 目标数在右侧
          left = mid;
          mid = Math.floor((left + right) / 2);
        } else {
          index = mid;
          return index;
        }
      }
      index = left;
      return index;
    },
    onScroll() {
      console.log(this.$refs.scrollsss.scrollTop);
      const scrollTop = this.$refs.scrollsss.scrollTop;
      console.log('scrollTop', scrollTop);
      let startIndex = this.getStartIndex(scrollTop);
      // 如果是奇数开始,就取其前一位偶数
      if (startIndex % 2 !== 0) {
        this.startIndex = startIndex - 1;
      } else {
        this.startIndex = startIndex;
      }
      this.endIndex = this.startIndex + this.visibleCount;
      this.scrollTop = this.itemTopCache[this.startIndex] || 0;
    }
  }
};
</script>
 
<style scoped>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
 
.phantom {
   border: solid 1px #eee;
  margin-top: 10px;
  height 600px
  overflow auto
}
 
ul {
  background: #ccc;
  list-style: none;
  padding: 0;
  margin: 0;
  li {
    outline: solid 1px #fff;
    &:nth-child(2n) {
      background: #fff;
    }
  }
}
</style>

外部引入组件 Item,列表循环项

<template>
  <li :key="item.brandId" :style="{height: `${item.height}px`, 'line-height': `${item.height}px`}" ref="node">
    <div>
      <div>{{item.name}}</div>
    </div>
  </li>
</template>
 
<script>
export default {
  props: {
    item: {
      default() {
        return {}
      },
      type: Object
    },
    
    index: Number
  },
  data() {
    return {
      
    }
  },
  mounted() {
    this.$emit('update-height', {height: this.$refs.node.getBoundingClientRect().height, index: this.index})
  }
}
</script>

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值