别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!

41,005 阅读4分钟

如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。

我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。

具体实现

首先,我们先模拟 500 条数据

const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数据

然后准备以下几个容器:

<template>
  <div class="view-container">
    <div class="content-container"></div>
    <div class="item-container">
      <div class="item"></div>
    </div>
  </div>
</template>
  • view-container是展示数据的可视区域,即可滚动的区域
  • content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条
  • item-container是实际渲染数据的区域
  • item则是具体渲染的数据

我们给这几个容器一点样式:

.view-container {
  height: 400px;
  width: 200px;
  border: 1px solid red;
  overflow-y: scroll;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.content-container {
  height: 1000px;
}

.item-container {
  position: absolute;
  top: 0;
  left: 0;
}

.item {
  height: 20px;
}

view-container固定定位并居中,overflow-y设置为scroll

content-container先给它一个1000px的高度;

item-container绝对定位,topleft都设为 0;

每条数据item给他一个20px的高度;

先把 500 条数据都渲染上去看看效果:

初始渲染

这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:

首先定义可视高度和每一条数据的高度:

const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度

用动态绑定样式的方式给元素加上高度:

<div class="view-container" :style="{ height: viewHeight + 'px' }">
  <div
    class="content-container"
    :style="{
        height: itemHeight * data.length + 'px',
      }"
  ></div>
  <div class="item-container">
    <div
      class="item"
      :style="{
          height: itemHeight + 'px',
        }"
    ></div>
  </div>
</div>

content-container 使用每条数据的高度乘以数据总长度来得到实际高度。

然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:

const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)

showData里的数据才是我们要在item遍历渲染的数据:

<div
  class="item"
  :style="{
          height: itemHeight + 'px',
        }"
  v-for="(item, index) in showData"
  :key="index"
>
  {{ item }}
</div>

接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:

  1. 根据滚动的高度除以每一条数据的高度得到起始索引
  2. 起始索引加上容器可以展示的条数得到结束索引
  3. 根据起始结束索引截取数据

具体代码如下:

const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
  // 获取滚动距离
  scrollTop.value = (e.target as HTMLElement).scrollTop;
  // 初始索引 = 滚动距离 / 每一项的高度
  const startIndex = Math.round(scrollTop.value / itemHeight.value);
  // 结束索引 = 初始索引 + 容器高度 / 每一项的高度
  const endIndex = startIndex + viewHeight.value / itemHeight.value;
  // 根据初始索引和结束索引,截取数据
  showData.value = data.slice(startIndex, endIndex);

  console.log(showData.value);
};

打印一下数据看看数据有没有改变:

滚动数据改变

可以看到数据是动态改变了,但是页面上却没有按照截取的数据来展示,这是因为什么呢? 查看一下元素:

问题

可以看到存放数据的元素 也就是 item-container 也跟着向上滚动了,所以我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度

<div
  class="item-container"
  :style="{
          transform: 'translateY(' + scrollTop + 'px)',
        }"
>
  <div
    class="item"
    :style="{
            height: itemHeight + 'px',
          }"
    v-for="(item, index) in showData"
    :key="index"
  >
    {{ item }}
  </div>
</div>

看效果:

效果

文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象等,大家可以尝试一下,并试着优化一下。

希望本文能够对你有帮助。