虚拟列表竟然如此简单

137 阅读2分钟

需求

不知道你是否遇到过这样的需求,甲方爸爸要求将所有的数据都显示出来,还不能分页,后端可是一次性返回3000多条数据啊,按照传统的方式是可以直接显示,那么就意味着你要创建3000多个节点,页面加载得多受影响,虽说现在的电脑算力和浏览器的内核都很强大,但是我们开发中还是要避免这样

解决思路

使用虚拟列表解决。

比如现在要显示10000条数据,那么我们可以在视口(眼睛能够看到的区域)内只显示20条数据,不管你如何滚动视口就显示20条,这样就只需要创建20个元素就行了。

  1. 固定每条数据的高度,动态计算出父容器的高度
  2. 监听列表的滚动事件,可以拿到scrollTop,用每条数据的高度除以scrollTop获取到滚动了多少条数据
  3. 根据滚动了多少条数据,然后切割列表补位已经切割的元素

实现代码

第一步:基本结构的搭建

需要外界传入每条数据的高度,和要显示多少条数据,根据每条数据的高度计算出父容器的高度

这里用到了css3的变量,利用继承,就不用操作列表的属性了 (如果不会,可以直接给每个item动态的设置高度,这样会操作20次属性,不过问题不大)

<template>
  <div
    class="virtual-list"
    :style="{
      height: viewPortHeigth,
      '--itemHeight': itemHeight + 'px',
    }"
  >
    <div class="scroll-bar" :style="{ height: scrollBarHeight }"></div>

    <div class="list" ref="listRef">
      <template v-for="item in showList">
      <!-- <div class="item" :style={height:itemHeight + 'px'}>{{ item }}</div> -->
        <div class="item">{{ item }}</div>
      </template>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  // 大数据的列表
  list: {
    type: Array,
    default: [],
  },
  // 每条数据的高度
  itemHeight: {
    type: String,
    default: '20',
  },
  // 显示多少条数据
  viewCount: {
    type: Number,
    default: 20,
  },
})

const start = ref(0)
const end = ref(props.viewCount)

// 切割大数据列表
const showList = computed(() => {
  return props.list.slice(start.value, end.value)
})
// 计算视口的高度
const viewPortHeigth = computed(() => {
  return props.viewCount * props.itemHeight + 'px'
})
// 容器的高度是通过每条数据的高度计算的 这会导致无法滚动
// 所以要通过大数据列表的所有数据*每条数据的高度把父容器撑起来
const scrollBarHeight = computed(() => {
  return props.list.length * props.itemHeight + 'px'
})

</script>

<style scoped lang="less">
.virtual-list {
  position: relative;
  border: 1px solid #ccc;
  overflow-y: auto;
  .list {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
  }
  .item {
    margin: 8px 0;
    height: var(--itemHeight);
  }
}
</style>

第二步

添加滚动事件,通过scrollTop除以每条数据的高度可以计算出已经滚了多少条数据,然后将滚动到上面遮住的数据从显示列表中去除,去掉几条数据就添加几条新的数据

const handleScroll = (e) => {
   
  const scrollTop = e.target.scrollTop
  // 可能会有小数,四舍五入取整
  let offset = Math.round(scrollTop / props.itemHeight)
  
  // 这里做个防抖的处理 因为滚动会触发很频繁
  if (offset === start.value) return
  
  // offset就是已经滚动多少条
  // 比如滚动了1条,那么就要讲切割的列表的第一条给切割掉
  // 然后在切割列表的最后添加一条,设置这两个属性 会触发计算属性重新计算
  start.value = offset
  end.value = start.value + props.viewCount
  // 虽然实现了切割效果,但是内容没有往下平移,所以滚动了多少px 就讲列表容器垂直平移多少px
  listRef.value.style.transform = `translateY(${scrollTop}px)`
}

完整代码

父组件

<template>
  <div class="container">
    <virtual-list :list="list" itemHeight="30" :viewCount="20"></virtual-list>
  </div>
</template>

<script setup>
import VirtualList from './virtualList.vue'

// 初始化数据
// 这里只是模拟一下数据
// 真实开发中可以监听列表的滚动事件 然后获取数据push到这个列表中
// 当然也可以一次性请求所有数据,这样就不用监听滚动事件了
const list = new Array(10000).fill('').map((itme, index) => ({
  index,
  size: '100px',
}))
</script>

<style scoped lang="less"></style>

子组件

<template>
  <div
    class="virtual-list"
    @scroll="handleScroll"
    :style="{
      height: viewPortHeigth,
      '--itemHeight': itemHeight + 'px',
    }"
  >
    <div class="scroll-bar" :style="{ height: scrollBarHeight }"></div>

    <div class="list" ref="listRef">
      <template v-for="item in showList">
        <div class="item">{{ item }}</div>
      </template>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  list: {
    type: Array,
    default: [],
  },
  itemHeight: {
    type: String,
    default: '20',
  },
  viewCount: {
    type: Number,
    default: 20,
  },
})
const listRef = ref(null)
const start = ref(0)
const end = ref(props.viewCount)

const handleScroll = (e) => {
  const scrollTop = e.target.scrollTop
  let offset = Math.round(scrollTop / props.itemHeight)
  console.log(offset)
  if (offset === start.value) return

  start.value = offset
  end.value = start.value + props.viewCount
  listRef.value.style.transform = `translateY(${scrollTop}px)`
}

const showList = computed(() => {
  return props.list.slice(start.value, end.value)
})
const viewPortHeigth = computed(() => {
  return props.viewCount * props.itemHeight + 'px'
})
const scrollBarHeight = computed(() => {
  return props.list.length * props.itemHeight + 'px'
})
</script>

<style scoped lang="less">
.virtual-list {
  position: relative;
  border: 1px solid #ccc;
  overflow-y: auto;
  .list {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
  }
  .item {
    margin: 8px 0;
    height: var(--itemHeight);
  }
}
</style>