虚拟滚动:静态虚拟滚动、动态虚拟滚动,实现起来有什么区别?

3,463 阅读6分钟

虚拟滚动

任何需要绘制、需要做界面映射的优化,性能都没有虚拟滚动性能好!!!虚拟滚动的概念就是只绘制再滚动条范围呢的。也有些万人游戏同屏其实可视区域就这么大点,也就是你再移动的时候绘制不同的人物、场景罢了!!!

图片

常用场景

罗列下再开发中经常会遇到需要使用的场景:

  • table表格 (在列比较多,还有产品需求就是不想要分页!!!)
  • select下拉菜单数据较多场景
  • list 像博客等平台展示信息列表如果用户一直往下加载其实当达到一定的阈值界面就会卡顿了。

虚拟滚动的实现

  • 撑起滚动条。
  • 滚动条滑动需要展示对应的数据。

虚拟滚动又分为静态、动态概念

静态虚拟滚动: 就是像表格、或者下拉菜单数据内如高度是一致的,不会有高低不一致的场景!!

动态虚拟滚动:子元素内容不一致、又或者由于需要展示文本等长短原因导致内容不一致!!!

固定子元素

在已知的容器高度固定子元素高度时可以轻松的绘制出来虚拟滚动效果。

图片

10000条数据,容器高度是500px,每个子元素高度50px, 那么总高度就是10000 * 50, 可视区域就是500 / 50 = 10条数据,缓冲区4条数据(前后2条,主要是为了让用户觉得体验感更强点)。

实现就是绘制一个div设置高度500px溢出出现滚动条, 里面绘制一个div设置min-height: 500000px;把高度撑起来。再绘制一个div填充的也就是视口数据,根据scrollTop值使用定位、margin-toptranslateY把位置移动到目前滚动后的视口区域。大致dom结构如下:

<div class="container" style="height: 500px; ooverflow: auto;">
  <div class="content" style="500000px">
    <div class="list" style="margin-top: 0px; height: 500px;">
        <div class="item">1</div>
        <div class="item">2</div>
        <div class="item">3</div>
        <div class="item">4</div>
        <div class="item">5</div>
        <div class="item">6</div>
        <div class="item">7</div>
        <div class="item">8</div>
        <div class="item">9</div>
        <div class="item">10</div>
        <div class="item">11</div>
        <div class="item">12</div>
        <div class="item">13</div>
        <div class="item">14</div>
    </div>
  </div>
</div>

scrollTop滚动条移动到500时这个时候,margin-top应该是600px(往上移动2行作为缓冲区),数据绘制的是[9,10,11,12,13,...20,21,22],其中9, 10, 21,22是咱们的缓冲区不显示的!!!界面实际显示的[10-20]数据。

那么这个算法怎么算的呢?如下:

  • 上面边界考虑scrollTop < 100 的时候 margin-top应该跟scrollTop是一致的。数据上应该是如下:
const originData = []; // 原始数据10000条数据
const ITEM_HEIGHT = 50;

if(scrollTop < ITEM_HEIGHT * 2) {
  this.marginTop = scrollTop;
  // Math.round 向下取整1.2 -> 1
  const offset = Math.round(scrollTop / ITEM_HEIGHT); // 用滚动条滑动的高度 / 子元素高度 得到当前需要展示数据的开始位置索引
  this.currentData =  originData.slice(offset, offset + 14);
}
  • 中间位置 scrollTop > 100 的时候 margin-top应该scrollTop + 100,上面缓冲2条数据。数据上应该是如下:
const originData = []; // 原始数据10000条数据
const ITEM_HEIGHT = 50;

if(scrollTop < ITEM_HEIGHT * 2) {
  this.marginTop = scrollTop + ITEM_HEIGHT * 2;
  // Math.round 向下取整1.2 -> 1
  const offset = Math.round(scrollTop / ITEM_HEIGHT) - 2; // 用滚动条滑动的高度 / 子元素高度 得到当前需要展示数据的开始位置索引
  this.currentData =  originData.slice(offset , offset + 14);
}
  • 下边界跟中间一样

封一个静态的虚拟滚动也是一样的简单你可能需要下面api

// 第一步获取容器
const container = document.querySelector('.container');
// 第二步获取容器的高度
const { height: containerHeight } = container.getBoundingClientRect(); 
// 第三步定义子元素高度
const ITEM_HEIGHT = 50;

const BUFFER = 4; // 缓冲区保留4个

const showItemSum = Math.ceil(containerHeight / 50); // 计算出来再当前容器里面能显示几个。

container.addEventListener('scroll', () => {
  const scrollTop = container.scrollTop;

  if(scrollTop < ITEM_HEIGHT * (BUFFER / 2)) {
    this.marginTop = scrollTop;
    const offset = Math.round(scrollTop / ITEM_HEIGHT);
    this.currentData =  originData.slice(offset, showItemSum + BUFFER);
   } else {
    this.marginTop = scrollTop + ITEM_HEIGHT * (BUFFER / 2); // margin
    const offset = Math.round(scrollTop / ITEM_HEIGHT) - (BUFFER / 2); 
    this.currentData =  originData.slice(offset , showItemSum + BUFFER); // 当前显示数据
   }
});

动态虚拟滚动

相比静态的主要难点再不好控制显示几条数据, 不知道下个元素的高度是多少,没办法得到子元素的高度!!那么如何解决这个问题呢?

如果接收一个min-height参数由用户传进来一个最小高度的值,把这个最小高度当作元素的固定高度去使用如上面(ITEM_HEIGHT),但还是有个问题由于每个子元素高度不一致,你没办法得到准确的marginTop!!! ,没办法计算出当前需要展示的数据!!!

chartgpt说:

实现动态虚拟滚动子元素高度不一致通常需要结合以下几个步骤进行设计:

  • 计算可视区域内能够展示多少个子元素。
  • 设置一个固定的容器高度,并根据每个子元素的高度对应生成一个高度数组。
  • 在渲染子元素时,只渲染当前视口区域内的子元素,而不是所有子元素,这可以使用CSS的 overflow: scroll 属性实现。
  • 随着用户向上或向下滚动视图,动态调整子元素的位置和内容,并根据当前展示子元素计算出需要设置的容器高度。
  • 当用户再次滚动到一个新的区域时,重复步骤 3 和 4,以实现无限滚动的效果。

总体来说,这种设计方法可以通过动态计算每个子元素的高度,从而使得即使在子元素高度不一致的情况下也能实现虚拟滚动的效果。同时,只渲染当前可视区域内的子元素也节省了浏览器渲染的开销,提高了性能表现。



如果我计算每个子元素的高度 就需要把每个子元素渲染一遍,这个样是能满足做虚拟滚动计算。这个样不是绝对不是最优的方案。当我参考vue-virtual-scroller了后发现他是这么做的如下:

  • 子元素绘制出来的也就有高度了记录出来放在size上, 没绘制出来过的用默认最小高度

  • 滚动条高度用再没有子元素高度用min-height计算(也就是那些还没绘制出来过的),绘制过的就正常计算,所以这个高度是随着鼠标滑动一直更新的!!!

  • scrollTop 也是同理,再有子元素高度就赋值给scrollTop差值。

关键部分代码:

export default {
  computed: {
    itemsWithSize () {
      const result = [];
      const { items, keyField, simpleArray } = this;
      const sizes = this.vscrollData.sizes; // 这个值会一直随着绘制子元素更新子元素高度,更新触发计算属性
      const l = items.length;
      for (let i = 0; i < l; i++) {
        const item = items[i];
        const id = simpleArray ? i : item[keyField];
        let size = sizes[id];
        if (typeof size === 'undefined' && !this.$_undefinedMap[id]) {
          size = 0;
        }
        result.push({
          item,
          id,
          size,
        });
      }
      return result;
    },
  },
  watch: {
    itemsWithSize (next, prev) {
      // next, prev 这一次、上一次值
      const scrollTop = this.$el.scrollTop;
      let prevActiveTop = 0; let activeTop = 0;
      const length = Math.min(next.length, prev.length);
      for (let i = 0; i < length; i++) {
        if (prevActiveTop >= scrollTop) {
          break;
        }
        prevActiveTop += prev[i].size || this.minItemSize;
        activeTop += next[i].size || this.minItemSize;
      }
      const offset = activeTop - prevActiveTop; // 用新老值计算
      if (offset === 0) {
        return;
      }
      this.$el.scrollTop += offset; // 设置scrollTop
    },
  },
};

vue-virtual-scroller

使用vue的可以直接去使用,尤大大推荐的哦!!支持静态、动态两种虚拟滚动!!!

演武场 可以去体验下!!

使用文档 快戳快戳!!!必须学会,有组件等于我学会哈哈哈

静态使用如下:

<template>
  <RecycleScroller
    class="scroller"
    :items="list"
    :item-size="32"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="user">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default {
  props: {
    list: Array,
  },

  components: {
    RecycleScroller,
  }
}
</script>

<style scoped>
.scroller {
  height: 100%;
}

.user {
  height: 32%;
  padding: 0 12px;
  display: flex;
  align-items: center;
}
</style>

动态虚拟滚动使用如下:

<template>
  <DynamicScroller
    :items="items"
    :min-item-size="54"
    class="scroller"
  >
    <template v-slot="{ item, index, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[
          item.message,
        ]"
        :data-index="index"
      >
        <div class="avatar">
          <img
            :src="item.avatar"
            :key="item.avatar"
            alt="avatar"
            class="image"
          >
        </div>
        <div class="text">{{ item.message }}</div>
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>

<script>
import { DynamicScroller,  DynamicScrollerItem } from 'vue-virtual-scroller'
export default {
  props: {
    items: Array,
  },

  components: {
    DynamicScroller,
    DynamicScrollerItem,
  }
}
</script>

<style scoped>
.scroller {
  height: 100%;
}
</style>

好用好用!!

vxe-table

vue 表格虚拟滚动组件,相信也有很多人使用过,这个就是存静态虚拟滚动,做的也是支持上下、左右两种也算是不错!!

使用文档

总结

通过本文是不是把虚拟滚动吃透了!! 期待您的点赞、收藏、关注!!!