听说你还不会虚拟列表?原谅我来晚了

15,346 阅读3分钟

什么是虚拟列表

虚拟列表是指对列表的 可视区域 进行渲染,对 非可见区域 不渲染或部分渲染,从而极大提高渲染性能的一种技术。

为什么要用虚拟列表

有时我们会遇到一些业务场景,要展示的列表很长,且不能使用分页的方式,如果一次性把数据全部渲染到页面,浏览器将变得非常卡顿,因为渲染 dom 需要耗费大量时间。虚拟列表 就是对长列表的一种优化方式,通过只渲染可视区域数据,大大提高渲染性能。

如何使用虚拟列表

目前虚拟列表已经有很多知名的库,如 vue-virtual-scrollervue-virtual-scroll-listreact-virtualized 等, 下面就给大家介绍一下 vue-virtual-scroller 这个优秀库的使用方法,然后再带大家实现一个简版的虚拟列表。准备好了吗,开干!

安装

npm install --save vue-virtual-scroller

RecycleScroller组件

适用于列表每一项高度确定的情况,高度可设置成相同,也可单独配置每一项高度。

src/components/virtualRecycleScroller.vue

<template>
  <RecycleScroller
    class="scroller"
    :items="items"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="desc">
      {{ item.label }}
    </div>
  </RecycleScroller>
  <!-- items: 需要渲染的列表,itemSize: 列表项的高度,keyField: 列表循环的key值 -->
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
  components: {
    RecycleScroller
  },
  props: {
    items: Array
  }
}
</script>

<style scoped>
.scroller {
  height: 100%;
}
.desc {
  height: 50px;
  line-height: 50px;
  text-align: center;
  box-sizing: border-box;
  border: 1px solid #ccc;
}
</style>

src/App.vue

<template>
  <div class="container">
    <virtual-recycle-scroller :items="list" />
  </div>
</template>

<script>
import virtualRecycleScroller from '@/components/virtualRecycleScroller'
// 模拟一个长列表
const list = []
for(let i=0; i<10000; i++) {
  list.push({
    id: i,
    label: `virtual-list ${i}`
  })
}
export default {
  components: {
    virtualRecycleScroller
  },
  data() {
    return {
      list: list
    }
  }
}
</script>

<style scoped>
.container {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

效果如下:

20211121_222152 00_00_00-00_00_30.gif

一万条数据的列表瞬间就渲染出来了,滚动也丝滑无比,是不是很nice!

DynamicScroller组件

适用于列表每一项高度不确定的情况。

src/components/virtualDynamicScroller.vue

<template>
  <DynamicScroller class="scroller" :items="items" :min-item-size="50">
    <template v-slot="{ item, index, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :size-dependencies="[item.label]"
        :data-index="index"
      >
        <div class="desc">{{ item.label }}</div>
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
  <!-- minItemSize: 列表项初次渲染使用的最小高度-->
  <!-- active: 保持视图,防止不必要的重新计算 -->
  <!-- sizeDependencies: 影响高度的值,如果发生变化,则重新计算 -->
</template>

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

<style scoped>
.scroller {
  height: 100%;
}
.desc {
  padding: 12px;
  text-align: center;
  border: 1px solid #ccc;
}
</style>

src/App.vue

<template>
  <div class="container">
    <virtual-dynamic-scroller :items="list" />
  </div>
</template>

<script>
import virtualDynamicScroller from '@/components/virtualDynamicScroller.vue'
// 模拟一个长列表
const list = []
for(let i=0; i<10000; i++) {
  list.push({
    id: i,
    label: `virtual-scroller ${i}`
  })
}
// 模拟一个内容不同的列表项
list[2].label = `virtual-scroller 2 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞
前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 
前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 前端阿飞 `
export default {
  components: {
    virtualDynamicScroller
  },
  data() {
    return {
      list: list
    }
  }
}
</script>

<style scoped>
.container {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

效果如下:

20211121_223340 00_00_01-00_00_06 00_00_00-00_00_30.gif

可以看到列表项的高度是随内容的变化而变化,依旧是丝滑无比!

虚拟列表原理

初次听到 “虚拟列表” 这个名词感觉非常的高大上,其实弄清楚它的原理之后,你会发现它非常的简单。话不多说,先上图:

image.png

  • 可视区容器:可以看作是在最底层,容纳所有元素的一个盒子。

  • 可滚动区域:可以看作是中间层,假设有 10000 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 10000 * 50。这一层的元素是不可见的,目的是产生和真实列表一模一样的滚动条。

  • 可视区列表:可以看作是在最上层,展示当前处理后的数据,高度和可视区容器相同。可视区列表的位置是动态变化的,为了使其始终出现在可视区域。

理解以上概念之后,我们再看看当滚动条滚动时,我们需要做什么:

  1. 根据滚动距离和 item 高度,计算出当前需要展示的列表的 startIndex
  2. 根据 startIndex 和 可视区高度,计算出当前需要展示的列表的 endIndex
  3. 根据 startIndexendIndex 截取相应的列表数据,赋值给可视区列表,并渲染在页面上
  4. 根据滚动距离和 item 高度,计算出可视区列表的偏移距离 startOffset,并设置在列表上

xnlb2.jpg

原理就是这些,不知道大家有木有听明白。俗话说 “书读百遍,其义自现” ,但我更相信 “实践出真知” ,接下来我们就自己动手实现一个虚拟列表吧!

手写一个简版的虚拟列表

src/components/myVirtualScroller.vue

<template>
  <!-- 最底层的可视区容器 -->
  <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
    <!-- 中间的可滚动区域,z-index=-1,高度和真实列表相同,目的是出现相同的滚动条 -->
    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <!-- 最上层的可视区列表,数据和偏移距离随着滚动距离的变化而变化 -->
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div
        class="infinite-list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{ height: itemSize + 'px' }"
      >
        {{ item.label }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MyVirtualList',
  props: {
    //列表数据
    items: {
      type: Array,
      default: () => []
    },
    //列表项高度
    itemSize: {
      type: Number,
      default: 50
    }
  },
  computed: {
    //列表总高度
    listHeight() {
      return this.items.length * this.itemSize
    },
    //可视区列表的项数
    visibleCount() {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    //可视区列表偏移距离对应的样式
    getTransform() {
      return `translate3d(0,${this.startOffset}px,0)`
    },
    //获取可视区列表数据
    visibleData() {
      return this.items.slice(this.start, Math.min(this.end, this.items.length))
    }
  },
  mounted() {
    this.screenHeight = this.$refs.list.clientHeight
    this.start = 0
    this.end = this.start + this.visibleCount
  },
  data() {
    return {
      screenHeight: 0, //可视区域高度
      startOffset: 0, //偏移距离
      start: 0, //起始索引
      end: 0 //结束索引
    }
  },
  methods: {
    scrollEvent() {
      //当前滚动位置
      let scrollTop = this.$refs.list.scrollTop
      //此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize)
      //此时的结束索引
      this.end = this.start + this.visibleCount
      //此时的偏移距离
      this.startOffset = scrollTop - (scrollTop % this.itemSize)
    }
  }
}
</script>

<style scoped>
.infinite-list-container {
  height: 100%;
  overflow: auto;
  position: relative;
}

.infinite-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.infinite-list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
}

.infinite-list-item {
  line-height: 50px;
  text-align: center;
  color: #555;
  border: 1px solid #ccc;
  box-sizing: border-box;
}
</style>

src/App.vue

<template>
  <div class="container">
    <my-virtual-scroller :items="list" />
  </div>
</template>

<script>
import myVirtualScroller from '@/components/myVirtualScroller'
// 模拟一个长列表
const list = []
for(let i=0; i<10000; i++) {
  list.push({
    id: i,
    label: `virtual-list ${i}`
  })
}
export default {
  components: {
    myVirtualScroller
  },
  data() {
    return {
      list: list
    }
  }
}
</script>

<style scoped>
.container {
  height: 300px;
  border: 1px solid #ccc;
}
</style>

效果和上面基本是一样: 20211128_152838 00_00_00-00_00_30.gif

打开控制台,可以看到页面始终只渲染了6条左右的数据:

20211129_212347 00_00_00-00_00_30.gif

以上就是简版的虚拟列表,大家可以自己动手试一试。如果要做的更加完善,还需考虑缓冲区域、列表项高度自适应等,有兴趣的同学可以自己研究一哈。

今天的分享就到这里,相信下次遇到这种场景你应该知道怎么处理了。你的支持就是我最大的动力,如果文章对你有所帮助,不要忘了点个免费的赞呦~😉

Demo地址:github.com/frontend-af…