如果让你渲染十万条数据,无脑全部渲染?拖出去斩了

2,093 阅读4分钟

前言:

何为虚拟列表?

回答这个问题先想一下,对于返回的几千上万条列表数据,作为前端会如何渲染?

分页呗,最开始想到的就是分页。但是,万一接口没做分页呢?好,就算后端做分页了,那么用户在浏览到第几千条、第几万条数据时前端难道要把这第几千条、几万条的DOM都渲染出来吗?显然是很消耗性能的(毕竟如此多的DOM,而且每个DOM内部都有其他样式细节)如何优化?此时就可以用到虚拟列表了

虚拟列表是一种优化长列表渲染的技术,它可以在保持流畅性的同时,渲染大量的数据。

在传统的列表渲染中,如果列表非常长,会导致渲染时间过长(前面所说的会有几千几万个DOM),页面卡顿,用户体验变得非常差。而虚拟列表则是只渲染可见区域内的数据,而非全部渲染,这样就可以大大提高渲染效率,保持页面流畅性

常见场景:

商品列表、社交列表...(暂时想到这两个)

手写虚拟列表:

进行虚拟列表的实现之前先搞清楚一个问题:

是什么造成了这么多数据在渲染时产生卡顿?是数据数量太多引起的吗?

准确来说,应该是是由于要渲染的DOM太多造成的卡顿。数据本身只是数据,对于拿到的几千上万条数据它本身的大小对于内存来说只能说是冰山一角吧

虚拟列表的原理:

数据我还是这么多数据,但是我不一次性渲染这么多数据,我只渲染其中的一小部分(比如十条),这样当用户滚动的时候就重复变化这一小部分的DOM的渲染效果。这样就能从原先几千上万个DOM变成现在的十个,减少了一个量级,减轻了渲染的压力。而这一小部分显示的区域暂称之为视图区域吧

审查元素,大致的效果如图:

贼长的这部分是所有的数据的盒子所占的大小,但是每次只控制小部分数据在上面的盒子进行显示

虚拟列表的实现:

怎么实现上面的效果?这里用到了固定定位和绝对定位

<script lang='ts' setup>
  type Item = {
    id: number
    name: string
  }
  const allListData = ref<Item[]>([])	// 存放十万条数据
  const itemHeight = ref(40) // 每一条(项)的高度
  const count = ref(10) // 一屏展示几条数据
  const startIndex = ref(0) // 开始位置的索引
  const endIndex = ref(10) // 结束位置的索引
  const topVal = ref(0) // 父元素滚动位置

  // 计算展示的列表
  const showListData = computed(() => allListData.value.slice(startIndex.value, endIndex.value))

  // 模拟十万条数据
  const getData = async () => {
    for (let i = 0; i < 10000; i++) {
      allListData.value.push({ name: `第${i}条数据`, id: i })
    }
  }

  // 初始化加载
  onMounted(() => {
    getData()
  })

  // 虚拟列表视口区域的组件实例
  const viewport = ref<HTMLDivElement>()

    const handleScroll = () => {
      console.log('滚动了')
      // 非空判断
      if (!viewport.value) return
      // 获取滚动距离(这里通过组件实例获取的,当然也可以通过在该事件的事件参数中拿到)
      const scrollTop = viewport.value.scrollTop
      // 计算起始下标和结束下标,用于 computed 计算
      startIndex.value = Math.floor(scrollTop / itemHeight.value)
      endIndex.value = startIndex.value + count.value
      // 动态更改定位的 top 值,动态展示相应内容
      topVal.value = viewport.value.scrollTop
    }
</script>

<template>
  <!-- 虚拟列表容器 -->
  <div
    class="viewport"
    ref="viewport"
    :style="{ height: 10条数据撑开的高度 }"
    >
    <!-- 占位元素,高度为所有的数据的总高度 -->
    <div
      class="placeholder"
      :style="{ height:itemHeight * count + 'px' }"
      ></div>
    <!-- 视图区,展示10条数据,注意其定位的top值是变化的 -->
    <div class="list" :style="{ top: topVal + 'px' }">
      <!-- 每一条(项)数据 -->
      <div
        v-for="item in showListData"
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
        >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

  <style scoped lang="scss">
  .viewport {
  box-sizing: border-box;
  width: 240px;
  border: solid 1px #000000;
  // 开启滚动条
  overflow-y: auto;
  // 开启相对定位
  position: relative;
  .list {
  width: 100%;
  height: auto;
  // 搭配使用绝对定位
  position: absolute;
  top: 0;
  left: 0;
  .item {
  box-sizing: border-box;
  width: 100%;
  height: 40px;
  line-height: 40px;
  text-align: center;
  // 隔行变色
  &:nth-child(even) {
  background: skyblue;
  }
  &:nth-child(odd) {
  background: #fff;
  }
  }
  }
  }
</style>
  • 一开始视图区域的top值为0,刚好在最顶端,监听滚动事件,当滚动时实时改变top值以及该区域内渲染的数据,从而实现了虚拟列表
  • 视图区域的top值为什么要动态监听?试想一下,现在所有数据的总高度为1000px,视图区域为100px,当用户滚动到500px时,如果视图区域的top值不动态绑定,那么视图区域还停留在top=0(也就是最顶端处),那肯定就看不到最新的视图了