基于betterScroll实现虚拟滚动

209 阅读1分钟

背景

为解决移动端常规列表分页数据加载过多时滑动卡顿和ios橡皮筋回弹问题,现基于betterScroll实现简单的每行定高的虚拟滚动列表

组件简单封装

<template>
  <div
    ref="wrapper"
    class="virtual-scroll-wrapper"
  >
    <div
      ref="content"
      class="virtual-scroll-content"
      :style="{
        paddingTop: topHeight+'px',
        paddingBottom: bottomHeight + 'px',
      }"
    >
      <div
        v-for="(item, index) in visibleItems"
        :key="index"
        class="virtual-scroll-item"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script>
// betterScroll源码
import BScroll from './BetterScroll' 

export default {
  props: {
    items: Array, // 列表项
    itemHeight: {
      type: Number,
      default: 78 // 每行高度
    }
  },
  data () {
    return {
      bs: null,
      scrollTop: 0,
      wrapperHeight: 0,
      contentHeight: 0
    }
  },
  computed: {
    startIndex () {
      let index = Math.floor(Math.abs(this.scrollTop) / this.itemHeight)
      index = Math.max(0, index)
      return index
    },
    endIndex () {
      let index = this.startIndex + Math.ceil(this.wrapperHeight / this.itemHeight)
      index = Math.min(index, this.items.length)
      return index
    },
    topHeight () {
      const arr = this.items.slice(0, this.startIndex)
      return arr.length * this.itemHeight
    },
    bottomHeight () {
      const restItems = this.items.slice(this.endIndex)
      return restItems.length * this.itemHeight
    },
    // 根据当前滚动位置和窗口大小计算可见项
    visibleItems () {
      return this.items
        .slice(this.startIndex, this.endIndex)
        .map(item => ({
          ...item
        }))
    }
  },
  watch: {
    items: {
      deep: true,
      handler () {
        this.refreshScroll()
      }
    }
  },
  mounted () {
    this.wrapperHeight = this.$refs.wrapper.offsetHeight
    this.$nextTick(() => {
      this.initBetterScroll()
    })
  },
  beforeDestroy () {
    if (this.bs) {
      this.bs.destroy()
    }
  },
  methods: {
    initBetterScroll () {
      this.bs = new BScroll(this.$refs.wrapper, {
        scrollX: true,
        scrollY: true,
        probeType: 3,
        click: true,
        bounce: {
          top: false,
          bottom: false,
          left: false,
          right: false
        }
      })

      this.bs.on('scroll', this.scrollHandler)
    },
    scrollHandler (pos) {
      this.scrollTop = pos.y
    },
    refreshScroll () {
      if (this.bs) {
        this.bs.refresh()
      }
    }
  }
}
</script>

<style scoped>
.virtual-scroll-wrapper {
  height: 100%;
  width: 100%;
  overflow: hidden;
  -webkit-overflow-scrolling: touch;
}

.virtual-scroll-content {
  display: inline-block;
  position: relative;
}

.virtual-scroll-item {
  padding: 20px;
  font-size: 28px;
  border-bottom: 1px solid #ccc;
}
</style>

组件使用

<template>
  <VList :items="dataList"/>
</template>

<script>
import VList from '@/components/VList.vue'
export default {
  components: {
    VList
  },
  data () {
    return {
      dataList: []
    }
  },
  created () {
    this.initDataList()
  },
  methods: {
    initDataList () {
      for (let i = 0; i < 50; i++) {
        this.dataList.push({ text: '啊急急急去我姐且企鹅企鹅却为何却文化请问请问' + i })
      }
    }
  }
}
</script>

说明

  • 此处只实现了简单的列表示例
  • 列表每项的高度是默认78px,暂未自适应
  • 列表每项可通过插槽自定义,此处暂未实现