面试官:如何设计一个定高的虚拟列表

458 阅读3分钟

前言

虚拟列表(Virtual List)是什么?这是一种前端性能优化的技术,主要是应用于渲染超长列表的场景。其核心原理是:只渲染可视区域内的列表项,而非全部数据,从而大幅度地减少了DOM节点的数量,提升页面性能。

最近刚好了解到虚拟列表的相关内容,虚拟列表主要有定高和不定高两种情景,其实现方法还挺有意思的。本文实现的是一个定高的虚拟列表,也就是说每一项的高度是固定的,因此我们可以通过计算得知整个列表的高度。

示意图

定高的虚拟列表的示意图如下所示。

定高的虚拟列表的示意图.png

主要分为以下几个部分:

  • 外部容器:固定高度(即可视区域的高度),超出隐藏;监听页面的 scroll 滚动事件。

  • 占位元素区域:设置的是所有内容的高度,用于撑开滚动区域,其高度会远远大于外部容器的高度以实现滚动的效果。(其实不加占位元素也是可以的,把 totalHeight 设置到 .scroll-content 元素)

  • 实际列表区域:实际渲染的列表内容是很少的,通过 transform 属性模拟滚动效果。

接下来详细分析其实现原理。

实现原理

首先,页面框架是这样设置的:容器是 .virtual-scroll,占位元素是 .scroll-phantom,实际渲染的列表是 .scroll-content,其中,每一项是<div class="item"></div>。而 totalHeight 是占位元素的总高度,是动态设置的。

<template>
  <div class="virtual-scroll" ref="container" @scroll="handleScroll">
    <!-- 占位元素,撑开滚动区域 -->
    <div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
    
    <!-- 实际渲染的列表 -->
    <div class="scroll-content" :style="{ transform: `translateY(${offset}px)` }">
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
    </div>
  </div>
</template>

<style scoped>
.virtual-scroll {
  height: 100%;
  overflow: auto;
  position: relative;
}

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

.scroll-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.item {
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box;
}
</style>

接着,我们不妨将列表每一项的高度固定为 60 px,总数据量 totalCount 定为 1000,那么 totalHeight 就是 60,000 px。

我们生成全部的模拟数据 allData,而可视区域的数据 visibleData 获取的是 allData 的 [startIndex, endIndex] 范围的数据。

那么,我们会有以下几个疑问:

1、我们需要考虑的是如何确定 startIndex 和 endIndex 的值?

将 startIndex 初始化为 0,在每次滚动页面时,动态更新。而 endIndex 的值为 startIndex + visibleCount。

在每次滚动时,startIndex 的值更新为 Math.floor(scrollTop / itemHeight),即滚动距离/每一项的高度。

2、我们怎么模拟滚动效果?

对于实际渲染的列表 .scroll-content ,通过设置样式{ transform: `translateY(${offset}px)` },offset 就是页面向上滑动的距离,在每次滚动页面时,动态更新即可。

3、怎么获取 offset 的值?

代码中,offset.value = scrollTop - (scrollTop % itemHeight),即列表的Y轴偏移量是页面的滚动距离 - (滚动距离 % 单项高度),这样是为了保证偏移量是单项高度的整数倍,使得内容在滚动时保持对齐,避免出现半截项的情况。

至此,核心原理就是这样。

理解了上面的几个问题,也就不难掌握定高的虚拟列表的实现方法了。

完整代码

完整的示例代码如下:

<template>
  <div class="virtual-scroll" ref="container" @scroll="handleScroll">
    <!-- 占位元素,撑开滚动区域 -->
    <div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
    
    <!-- 实际渲染的列表 -->
    <div class="scroll-content" :style="{ transform: `translateY(${offset}px)` }">
      <div 
        v-for="item in visibleData" 
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const itemHeight = 60 // 每项高度
const totalCount = 1000 // 总数据量

// 生成模拟数据
const allData = Array.from({ length: totalCount }, (_, i) => ({
  id: i,
  content: `Item ${i + 1}`
}))

const container = ref(null)
const containerHeight = ref(0)
const startIndex = ref(0)
const offset = ref(0)
const visibleCount = ref(0)

// 计算属性
const totalHeight = computed(() => allData.length * itemHeight)
const endIndex = computed(() => startIndex.value + visibleCount.value)
const visibleData = computed(() => allData.slice(startIndex.value, endIndex.value))

// 初始化
onMounted(() => {
  containerHeight.value = container.value.clientHeight
  visibleCount.value = Math.ceil(containerHeight.value / itemHeight) + 1
})

// 滚动处理
function handleScroll() {
  const scrollTop = container.value.scrollTop
  startIndex.value = Math.floor(scrollTop / itemHeight)
  offset.value = scrollTop - (scrollTop % itemHeight)
}
</script>

<style scoped>
.virtual-scroll {
  height: 100%;
  overflow: auto;
  position: relative;
}

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

.scroll-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.item {
  display: flex;
  align-items: center;
  padding: 0 16px;
  border-bottom: 1px solid #eee;
  box-sizing: border-box;
}
</style>

后记

简而言之,定高的虚拟列表是一种前端性能优化的技术,可以应用于渲染超长列表的场景。

开卷有益!