Vue2 - vue-virtual-scroller 长列表优化原理

vue-virtual-scroller

1,效果展示

在这里插入图片描述

1w 条数据无压力,看下初始渲染时间 Rendering 对比:

在这里插入图片描述
在这里插入图片描述

2,原理

目标:只加载在可视容器中的列表项。

实现:

2.1,滚动条的处理

  1. 为了保证滚动条正确显示和滑动,需要按照原本的数据量来计算滑动内容区域 wrapper 的真实高度,
  2. wrapper 外层的 container 作为滑动容器,设置 overflow:auto + 定高(比如500px)。

2.2,展示内容处理

最终目标:只展示可视区域内的 item,所以肯定会对源数据进行截取,需要计算出 startIndexendIndex

  1. 为了保证只显示可视区域内的 item,可通过定位将每个 item 的初始位置都固定在第一行(top: 0; left: 0)。
  2. 在对每个 item 设置不同的偏移量 transform: translateY(index * itemHeight); 就可以正常展示了。
  3. 滑动时,需要根据滑动的距离 container.scrollTop 来重新计算 startIndex。再加上 container.clientHeight 可以计算出 endIndex
  4. 则可得出初始偏移量:transform: translateY(startIndex* itemHeight);

3,实现

<!-- 父组件 -->
<template>
  <div>
    <RecycleScroller :items="list" :itemSize="54" v-slot="{ item }" class="scroller">
      <div class="item-box">
        <span>{{ item.id }}</span>
        <span>{{ item.name }}</span>
      </div>
    </RecycleScroller>
  </div>
</template>

<script>
import RecycleScroller from './components/RecycleScroller.vue'

const arr = []
for (let index = 0; index < 10000; index++) {
  arr[index] = {
    id: 'id' + index,
    name: `name` + index
  }
}

export default {
  components: {
    RecycleScroller
  },
  data() {
    return {
      list: arr
    }
  }
}
</script>
<style>
.scroller {
  width: 200px;
  height: 500px;
  overflow: auto;
}

.item-box {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px;
}
</style>
<!-- RecycleScroller.vue -->
<template>
  <div class="recycle-container" ref="container" @scroll="setPool">
    <div class="recycle-wrapper" :style="{ height: totalSize }">
      <div v-for="poolItem in pool" :key="poolItem.keyField" class="recycle-item" :style="{ transform: `translateY(${poolItem.position}px)` }">
        <slot :item="poolItem.item"></slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    items: {
      // 数据列表
      type: Array,
      default: () => []
    },
    itemSize: {
      // 每条数据的高度
      type: Number,
      default: 50
    },
    keyField: {
      // items 中的唯一标识作为 key
      type: String,
      default: 'id'
    }
  },
  data() {
    return {
      pool: [] // 会被渲染的列表内容
    }
  },
  computed: {
    totalSize() {
      return this.items.length * this.itemSize + 'px'
    }
  },
  methods: {
    setPool() {
      const scrollTop = this.$refs.container.scrollTop
      const clientHeight = this.$refs.container.clientHeight
      
      let startIndex = Math.floor(scrollTop / this.itemSize) || 0
      let endIndex = Math.ceil((scrollTop + clientHeight) / this.itemSize)
      const startPosition = startIndex * this.itemSize
      
      // 每次都重新计算 item 对应的位置。
      this.pool = this.items.slice(startIndex, endIndex).map((item, index) => ({
        item,
        position: startPosition + this.itemSize * index
      }))
    }
  },
  mounted() {
    this.setPool()
  }
}
</script>

<style scoped>
.recycle-container {
  overflow: auto;
}
.recycle-wrapper {
  position: relative;
}
.recycle-item {
  position: absolute;
  width: 100%;
  top: 0;
  left: 0;
}
</style>

如果担心滑动太快导致的白屏问题(没有计算渲染出来),可以在前后各增加10条数据,一般就没有问题了。

以上。

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下雪天的夏风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值