虚拟列表

Nov 23, 2022

todo:待整理

详情请看:https://www.huyu001.top/posts/21040.html

<!-- 虚拟列表:https://juejin.cn/post/7168645862296879117#heading-16 -->
<template>
  <div id="app" style="width: 100%">
    <div class="container" ref="virtualList">
      <div class="phantom" :style="{ height: listHeight + 'px' }"></div>
      <div class="content" ref="content" :style="{ transform: `translate3d(0, ${currentOffset}px, 0)` }">
        <div v-for="item in visibleData" :key="item.id" :id="item.id" ref="items" class="list-item">
          {{ item.value }}
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  import lodash from 'lodash'
  let binarySearch = function (list, target) {
    const len = list.length
    let left = 0,
        right = len - 1
    let tempIndex = null
    while (left <= right) {
      let midIndex = (left + right) >> 1
      let midVal = list[midIndex].bottom
      if (midVal === target) {
        return midIndex
      } else if (midVal < target) {
        left = midIndex + 1
      } else {
        // list不一定存在与target相等的项,不断收缩右区间,寻找最匹配的项
        if (tempIndex === null || tempIndex > midIndex) {
          tempIndex = midIndex
        }
        right--
      }
    }
    // 如果没有搜索到完全匹配的项 就返回最匹配的项
    return tempIndex
  }
  export default {
    name: 'newTest',
    data() {
      return {
        listData: [], // 列表数据
        positions: [], // 记录索引 偏移...
        preItemSize: 50,
        screenHeight: 0,
        currentOffset: 0,
        start: 0, // 开始
        end: 0, // 结束
        // count: 0,
        bufferPercent: 0.5, // 即每个缓冲区只缓冲 0.5 * 最大可见列表项数 个元素
      }
    },
    created() {
      for (let i = 1; i <= 1000; i++) {
        this.listData.push({id: i, value: i + '字符内容'.repeat(Math.random() * 20)})
      }
      this.initPositions(this.listData, this.preItemSize)
    },
    mounted() {
      this.screenHeight = this.$el.clientHeight
      this.start = 0
      this.end = this.start + this.visibleCount
      // 绑定滚动事件
      let target = this.$refs.virtualList
      let scrollFn = event => this.scrollEvent(event.target)
      let debounce_scroll = lodash.debounce(scrollFn, 160)
      let throttle_scroll = lodash.throttle(scrollFn, 80)
      target.addEventListener('scroll', debounce_scroll)
      target.addEventListener('scroll', throttle_scroll)
      // target.addEventListener("scroll",  scrollFn);
      console.log(this.positions, this.visibleData)
    },
    updated() {
      this.$nextTick(() => {
        if (!this.$refs.items || !this.$refs.items.length) {
          return
        }
        // 根据真实元素大小,修改对应的缓存列表
        this.updatePositions()
        // 更新完缓存列表后,重新赋值偏移量
        this.currentOffset = this.getCurrentOffset()
      })
    },
    computed: {
      listHeight() {
        return this.positions[this.positions.length - 1].bottom
      },
      visibleCount() {
        return Math.ceil(this.screenHeight / this.preItemSize)
      },
      visibleData() {
        return this.listData.slice(this.start - this.aboveCount, this.end + this.belowCount)
      },
      bufferCount() {
        return (this.visibleCount * this.bufferPercent) >> 0 // 向下取整
      },
      // 使用索引和缓冲数量的最小值 避免缓冲不存在或者过多的数据
      aboveCount() {
        return Math.min(this.start, this.bufferCount)
      },
      belowCount() {
        return Math.min(this.listData.length - this.end, this.bufferCount)
      },
    },
    methods: {
      // 滚动回调
      scrollEvent(target) {
        const {scrollTop} = target
        this.start = this.getStartIndex(scrollTop)
        this.end = this.start + this.visibleCount
        this.currentOffset = this.getCurrentOffset()
      },
      // 初始化列表
      initPositions(listData, itemSize) {
        this.positions = listData.map((item, index) => {
          return {
            index,
            top: index * itemSize,
            bottom: (index + 1) * itemSize,
            height: itemSize,
          }
        })
      },
      // 渲染后更新positions
      updatePositions() {
        let nodes = this.$refs.items
        nodes.forEach(node => {
          // 获取 真实DOM高度
          const {height} = node.getBoundingClientRect()
          // 根据 元素索引 获取 缓存列表对应的列表项
          const index = node.id - 1
          let oldHeight = this.positions[index].height
          // dValue:真实高度与预估高度的差值 决定该列表项是否要更新
          let dValue = oldHeight - height
          // 如果有高度差 !!dValue === true
          if (dValue) {
            // 更新对应列表项的 bottom 和 height
            this.positions[index].bottom = this.positions[index].bottom - dValue
            this.positions[index].height = height
            // 依次更新positions中后续元素的 top bottom
            for (let k = index + 1; k < this.positions.length; k++) {
              this.positions[k].top = this.positions[k - 1].bottom
              this.positions[k].bottom = this.positions[k].bottom - dValue
            }
          }
        })
      },
      getStartIndex(scrollTop = 0) {
        return binarySearch(this.positions, scrollTop)
      },
      getCurrentOffset() {
        if (this.start >= 1) {
          // 计算偏移量时包括上缓冲区的列表项
          let size =
              this.positions[this.start].top -
              (this.positions[this.start - this.aboveCount]
                  ? this.positions[this.start - this.aboveCount].top
                  : 0)
          return this.positions[this.start - 1].bottom - size
        } else {
          return 0
        }
      },
    },
  }
</script>
<style scoped>
  .container {
    width: 500px;
    position: relative;
    height: 50vh;
    overflow: auto;
  }
  .phantom {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    height: 800px;
  }
  .content {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    text-align: center;
  }
  .list-item {
    padding: 10px;
    border: 1px solid #999;
  }
</style>
Back