我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。
首先,我们先模拟 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>
  1. view-container是展示数据的可视区域,即可滚动的区域
  2. content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条
  3. item-container是实际渲染数据的区域
  4. item则是具体渲染的数据

view-container固定定位并居中,overflow-y设置为scroll;
content-container先给它一个1000px的高度;
item-container绝对定位,top和left都设为 0;
每条数据item给他一个20px的高度;
先把 500 条数据都渲染上去看看效果:

.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;
}

image.png

这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定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添加滚动事件来动态改变要展示的数据,具体思路就是:

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

具体代码如下:

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);
};

我们不要让它滚动,可以通过调整它的 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>

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

原生js完整demo

<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="UTF-8">  
<meta name="viewport" content="width=device-width, initial-scale=1.0">  
<title>Virtual Scrolling Example</title>  
<style>  
  #container {  
    width: 300px;  
    height: 400px;  
    overflow-y: auto;  
    position: relative;  
  }  
  .item {  
    height: 50px;  
    line-height: 50px;  
    text-align: center;  
    border-bottom: 1px solid #ccc;  
  }  
</style>  
</head>  
<body>  
<div id="container"></div>  
  
<script>  
  // 模拟的数据源  
  const items = new Array(500).fill(0).map((_, x) => x);  
  const container = document.getElementById('container');  
  const itemHeight = 50; // 每个项目的高度  
  const bufferSize = 3; // 缓冲区大小,即在可视区域外的额外渲染数量  
  
  let renderedItems = new Set(); // 已渲染的项目集合

  // 渲染函数  
  function render(startIndex, endIndex) {  
    for (let i = startIndex; i <= endIndex; i++) {
      if (!renderedItems.has(i)) {
        const item = document.createElement('div');
        item.className = 'item';
        item.textContent = `Item ${i}`;
        container.appendChild(item);
        renderedItems.add(i);
      }
    }
  }  
  
  // 计算可视区域的起始和结束索引  
  function calculateVisibleRange() {  
    const scrollTop = container.scrollTop;  
    const visibleStartIndex = Math.floor(scrollTop / itemHeight) - bufferSize;  
    const visibleEndIndex = Math.min(items.length - 1, Math.floor((scrollTop + container.clientHeight) / itemHeight) + bufferSize);  
    return { visibleStartIndex, visibleEndIndex };  
  }  
  
  // 滚动事件处理  
  container.addEventListener('scroll', function() {  
    const { visibleStartIndex, visibleEndIndex } = calculateVisibleRange();  
    render(visibleStartIndex, visibleEndIndex);  
  });  
  
  // 初始渲染  
  const { visibleStartIndex, visibleEndIndex } = calculateVisibleRange();  
  render(visibleStartIndex, visibleEndIndex);  
</script>  
</body>  
</html>

得鹿梦鱼
2 声望0 粉丝