custom-list-view.uvue 4.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
<template>
  <scroll-view class="custom-list-view-scoll-view" v-bind="$attrs" ref="scroll" @scroll="onScroll">
    <view class="custom-list-view-placeholder" :style="{ height: placeholderHeight + 'px' }">
      <view class="custom-list-view-container" :style="{ top: containerTop + 'px' }">
        <slot :items="items"></slot>
      </view>
    </view>
  </scroll-view>
</template>

<script>
  /**
   * 使用限制
   * - 容器大小变动时未刷新缓存的子元素大小
   * - 不支持设置初始滚动位置
   * - list数据内每一项不可以是基础类型
   * - item不支持设置margin,会导致计算位置不准确
   */
  export default {
    name: "custom-list-view",
    props: {
      list: {
        type: Array as PropType<any[]>,
        default: [] as any[]
      }
    },
    watch: {
      list: {
        handler(list : any[]) {
          this.cachedSize.forEach((_ : number, key : any) => {
            if (!list.includes(key)) {
              this.cachedSize.delete(key)
            }
          })
        },
        deep: true
37 38 39
      },
      defaultItemSize() {
        this.rearrange(this.lastScrollTop)
40 41 42 43 44 45 46 47 48 49 50 51 52
      }
    },
    data() {
      return {
        items: [] as any[],
        containerTop: 0,
        scrollElementHeight: 0,
        placeholderHeight: 0,
        offsetThreshold: [0, 0, 0, 0], // -5, -3, 3, 5 屏对应的offset
        cachedSize: new Map<any, number>(),
        initialized: false,
        hasDefaultSize: false,
        defaultItemSize: 40,
53
        lastScrollTop: 0,
54
        rearrangeQueue: [] as number[]
55 56 57 58
      };
    },
    provide() {
      return {
59 60 61 62 63 64 65
        setCachedSize: (item : any, size : number) => {
          if (!this.hasDefaultSize) {
            this.defaultItemSize = size
            this.hasDefaultSize = true
          }
          this.cachedSize.set(item, size)
        }
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
      }
    },
    created() {
      this.placeholderHeight = this.list.length * this.defaultItemSize
    },
    mounted() {
      nextTick(() => {
        uni.createSelectorQuery().in(this).select('.custom-list-view-scoll-view').boundingClientRect().exec((ret) => {
          this.scrollElementHeight = (ret[0] as NodeInfo).height!
          this.rearrange(0)
          this.initialized = true
        })
      })
    },
    methods: {
      onScroll(e : UniScrollEvent) {
        if (!this.initialized) {
          return
        }
        const scrollTop = e.detail.scrollTop
86
        this.lastScrollTop = 0
87
        if (scrollTop < this.offsetThreshold[1] || scrollTop > this.offsetThreshold[2]) {
88
          this.queue(scrollTop)
89 90
        }
      },
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
      queue(scrollTop : number) {
        /*
         * rearrange内为大量同步逻辑,在上次rearrange未执行完毕的情况下将后续多个rearrange合并成一次执行,即仅执行最后一次
         * 由于滚动机制差异,此优化仅在web端才有意义。
         * 如何测试:push后console.log(this.rearrangeQueue.length) 输出结果大于1时触发优化
         */
        this.rearrangeQueue.push(scrollTop)
        setTimeout(() => {
          this.flush()
        }, 1)
      },
      flush() {
        const queueLength = this.rearrangeQueue.length
        if (queueLength === 0) {
          return
        }
        const lastScrollTop = this.rearrangeQueue[queueLength - 1]
        this.rearrange(lastScrollTop)
        this.rearrangeQueue = [] as number[]
      },
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
      rearrange(scrollTop : number) {
        this.offsetThreshold[0] = Math.max(scrollTop - this.scrollElementHeight * 5, 0)
        this.offsetThreshold[1] = Math.max(scrollTop - this.scrollElementHeight * 3, 0)
        this.offsetThreshold[2] = Math.min(scrollTop + this.scrollElementHeight * 4, this.placeholderHeight)
        this.offsetThreshold[3] = Math.min(scrollTop + this.scrollElementHeight * 6, this.placeholderHeight)
        const items = [] as any[]
        let tempTotalHeight = 0
        let containerTop = 0
        for (let i = 0; i < this.list.length; i++) {
          const item = this.list[i]
          let itemSize = this.defaultItemSize
          const cachedItemSize = this.cachedSize.get(item)
          if (cachedItemSize != null) {
            itemSize = cachedItemSize
          }
          tempTotalHeight += itemSize
          if (tempTotalHeight >= this.offsetThreshold[0] && tempTotalHeight <= this.offsetThreshold[3]) {
            items.push(item)
          } else if (tempTotalHeight < this.offsetThreshold[0]) {
            containerTop = tempTotalHeight
          }
        }
        this.placeholderHeight = tempTotalHeight
        this.items = items
        this.containerTop = containerTop
      }
    }
  }
</script>

<style>

143
</style>