Vue3实现虚拟列表的示例代码

 更新时间:2024年11月07日 11:26:08   作者:E_Yen  
虚拟列表是一种优化长列表渲染的技术,它可以在保持流畅性的同时,渲染大量的数据,本文主要介绍了如何通过Vue3实现一个虚拟列表,感兴趣的可以了解下

前言

本文的虚拟列表基于上一篇动态高度虚拟列表原理解析中的核心代码和Vue3实现,上文是看懂思考&设计部分内容的前提条件。

使用

安装

npm install @e.yen/virtual-scroll-vue

在main.js中显式导入或者在组件中按需导入

// main.ts
import VirtualScroll from '@e.yen/virtual-scroll-vue'
app.use(VirtualScroll)

// 或者
// AnyComponent.vue
import { VirtualScroll } from '@e.yen/virtual-scroll-vue'

导入样式

// main.ts
import '@e.yen/virtual-scroll-vue/dist/style.css'

Github仓库:virtual-scroll-vue

props参数

参数类型默认值是否必须描述
itemsVirtualScrollItem[]-✔️列表数据
placeholderVirtualScrollItem-最小子项的模拟数据
startPosition[number, number][0, 0]列表初始位置
preservednumber-子项的最小高度
paddingnumber100预渲染区域高度
type VirtualScrollItem = {
  key?: any
  height?: number // 可以指定元素高度,具有最高优先级
  [k: string | symbol]: any
}

注意preserved 的优先级高于 placeholder

expose方法

方法参数返回值类型描述
scrolldelta: number, duration?: number-滚动指定距离
transportnewStartPosition: [number, number]-传送到指定位置
getPosition-[number, number]获取列表当前位置

注意事项

让子项拥有唯一的key

由于列表基于v-for渲染子项,因此为子项拥有唯一的key能够大幅度提升性能表现:

const items = [
  {
    key: 'ABC',
  },
  {
    key: 'BCD',
  },
]

任何时候都不要使最小高度为0

由于元素在被渲染之前无法确认其高度,因此列表依赖于子项目的最小高度确定渲染索引范围。虽然能够通过 placeholder 将最小高度设为0,但这会导致列表渲染后续所有子项:

<!-- preserved默认具有最小值5px,设为0不会有任何效果 -->
<VirtualScroll :items="data" :preserved="0">
  <template #default="{ item }">
    <!-- 子项目结构 -->
  </template>
</VirtualScroll>

<!-- 通过placeholder将最小高度设为0,会导致列表一次性渲染所有元素 -->
<VirtualScroll :items="data" :placeholder="{}">
  <template #default="{ item }">
    <!-- 高度为0的子项目 -->
    <div></div>
  </template>
</VirtualScroll>

不要向起始元素之前添加新数据

由于列表的渲染索引范围由起始元素索引、起始元素偏移量和最小项目高度共同决定,因此向起始元素之前的位置添加新元素会导致意料之外的结果:

// 假设 startIndex 为 1
// 向items头部添加新数据,会导致列表渲染的起始元素变为旧数组中索引为 0 的项
items.unshift({
  key: 'CDE',
})

不要修改预渲染区内的元素高度

具体来说是不要修改靠近列表排列起始方向一侧的元素高度(例如列表从上往下排列,则不要修改上方预渲染区域内的元素高度)

不是强制要求的,相反,列表仍能正常工作,但是对于具有过渡效果的高度变化,受制于 ResizeObserver 的滞后性,列表可能出现微小的抖动导致用户体验变差

仅在触屏设备上使用

虽然列表支持滚轮滚动,但是暂不支持滚动条,在PC等非触屏设备上应考虑使用分页

示例

列表会自动获取可视区域大小,宽高默认为 100%,建议通过 .virtual-scroll_container 进行覆盖,或者在组件外部包裹一个容器

列表会将要渲染的数据通过默认作用域插槽传递出来,天然支持动态高度

<script setup lang="ts">
import { ref } from 'vue'
import DynamicItem from '@/components/DynamicItem/DynamicItem.vue'
import { VirtualScroll, type VirtualScrollInstance } from '@e.yen/virtual-scroll-vue'
import {
  generateRandomFirstWord,
  generateRandomWord,
  lorem,
} from '@/utils/helper'
const defaultItem = { name: 'ab', comment: 'abc', index: -1 }
const items = ref(
  new Array(10000).fill(0).map((_, i) => ({
    key: i.toString(),
    name:
      generateRandomFirstWord() +
      (Math.random() > 0.5
        ? ' ' + generateRandomWord(Math.floor(Math.random() * 8) + 2)
        : ''),
    comment: lorem(Math.floor(Math.random() * 5) + 1),
    index: i,
  })),
)

const vlist = ref<VirtualScrollInstance>()
function lighteningScroll(delta: number) {
  vlist.value!.scroll(delta)
}
</script>

<template>
  <div class="page">
    <div class="scroll_container">
      <VirtualScroll
        ref="vlist"
        :items="items"
        :placeholder="defaultItem"
        :start-position="[1000, 0]"
        :padding="0"
      >
        <template #default="{ item }">
          <DynamicItem
            :index="item.index"
            :name="item.name"
            :comment="item.comment"
          ></DynamicItem>
        </template>
      </VirtualScroll>
    </div>
    <button @click="lighteningScroll(-100000)">向上极速滚动测试</button>
    <button @click="lighteningScroll(100000)">向下极速滚动测试</button>
  </div>
</template>

思考&设计

虚拟列表的关键在于如何获取列表项高度,确定了每项的高度,就能确定渲染多少个元素。即如何获取列表项高度决定了虚拟列表的实际表现,在这里给出三个思路:

1.固定步长

在浏览器每一帧渲染之前进行判断,若虚拟列表中的元素不足以占满整个可视区域且仍有未被渲染的后续元素,则将渲染结束的索引后移 n 位。

  • 优点:实现简单直观
  • 缺点:引入了超参数 n,需要依照实际情况确定一个较为合理的值,较大则造成较多性能浪费,较小则容易导致用户快速滚动时出现空白页

2.预渲染 + 固定步长

原理与上述思路没有区别,优缺点与上面一致,可以认为是在计算元素是否足以占满可视区域时,将参与计算的可视区域进行扩大,从而让列表提前渲染元素。能够在一定程度上缓解空白问题,但治标不治本,当以更快的速度滚动(比如通过代码触发)时仍会出现空白页。

3.预渲染 + 高度预测

观察发现,出现空白页的根本原因是无法确定究竟最多还需要多少个元素才能占满可视区域,为此,可以通过每项的最小高度预测最多需要向后渲染多少个子项,从而保证始终有足够的元素占满可视区域。

  • 优点:通过高度预测彻底解决了空白问题
  • 缺点:在快速滚动时,由于实际高度与预测高度可能不同,可能会导致落点位置与预期不符

预测实现

高度预测主要有两种实现方式:

  • 在设计时就确定好最小高度,单位为CSS像素。实现最为简单且性能最高,但是对于一些使用了相对单位的项目结构不友好
  • 根据项目结构自动获取最小高度。通过传入一个具有最小高度的元素的模拟数据,列表动态地获取最小高度,通用性最好

错位处理

维持前文的约定:

起始位置 startPosition[startIndex, offset] 二元组构成

渲染信息 renderInfo[viewHeight, paddingHeight, listHeight] 三元组构成

函数 move(startPosition, delta, renderInfo, getHeight) => void 通过起始位置、移动距离、渲染信息和高度获取函数计算本次移动后的新起始位置

高度预测会导致快速滚动时出现到达的位置与预期不同的错位问题。粗略地看,当一帧内列表滚动到了未渲染区域,就会转为使用预测高度继续计算下一帧的起始位置,预测高度与实际高度不一致时就会导致列表“移动过头”。举个例子,假设某个未被渲染的元素实际高度为 110px,但在计算时将其视为 100px,那么剩余滚动距离就多了 10px,这是造成错位的根本原因。

既然知道了问题,那么研究其发生条件变得十分重要:

根据移动函数 move 的计算方式得知:

单次移动中,如果:

  • 向上移动距离大于 max(offset - paddingHeight, 0) + 预测高度
  • 向下移动距离大于 listHeight + 预测高度

就可能导致错位

由于虚拟列表是由起始位置决定的,因此向上滚动时的错位将是致命的。原因是计算新的 offset 时使用了预测的高度,但实际高度大于预测高度,导致后续所有元素都下移。在这里使用了自定义指令 + ResizeObserver的方式解决,处理过程分为3步:

v-auto-record 在元素被挂载时,缓存本次计算时使用的高度

v-watch-size 在元素高度被缓存后调用 elementResize 进行处理

elementResize 根据情况更新高度缓存,以及选择修改 offset 或重新渲染

// vAutoRecord.ts
export default <Directive>{
  mounted(el, binding) {
    if (binding.arg && binding.arg === 'mounted') binding.value?.(el)
  },
  unmounted(el, binding) {
    if (binding.arg && binding.arg === 'unmounted') binding.value?.(el)
  },
}

// vWatchSize.ts
export default <Directive>{
  mounted(el, binding) {
    // ! nextTick保证vWatchSize在vAutoRecord之后执行
    nextTick(() => {
      if (binding.value instanceof Function) binding.value(el)
      el.observer = new ResizeObserver(() => {
        if (binding.value instanceof Function) binding.value(el)
      })
      el.observer.observe(el)
    })
  },
  beforeUnmount(el) {
    if (el.observer) {
      el.observer.disconnect()
      delete el.observer
    }
  },
}
<li
  v-for="(i, index) in renderRange"
  :key="props.items[i].key || index"
  class="virtual-scroll_item"
  v-watch-size="el => elementResize(i, el)"
  v-auto-record:mounted="el => elementMap.set(i, el)"
  v-auto-record:unmounted="() => elementMap.delete(i)"
>
  <slot :item="props.items[i]"></slot>
</li>
const elementResize = (index: number, element: HTMLElement) => {
  const cur = element.getBoundingClientRect().height
  const pre = getHeight(index) // 取出高度缓存
  let isInPaddingRange = false
  if (cur === pre) return

  // 判断高度变化的元素是否在预加载区间
  let offset = startPosition.value[1]
  let itemIndex = startPosition.value[0]
  let height = getHeight(itemIndex)
  while (height >= 0 && offset > 0) {
    if (itemIndex === index) {
      isInPaddingRange = true
      break
    }
    offset -= height
    height = getHeight(++itemIndex)
  }

  // 更新高度缓存
  updateHeight(index)

  if (isInPaddingRange) {
    // 如果高度变化的元素在预加载区间内,将offset加上高度变化量
    startPosition.value[1] += cur - pre
  } else if (cur < pre) {
    // 如果高度变化的元素不在预加载区间内,重新渲染
    renderTrigger.value = !renderTrigger.value
  }
}

至于向下滚动时的错位问题,这是高度预测的固有局限,因此没有很好的解决方法,一种可能的蒙混过关的解决方式是:快速滚动时用户无法分辨页面上到底呈现了什么,可以在滚动结束的下一帧立即将起始位置修改为目标位置,实现向下快速滚动到指定位置的错觉。但如果在列表项中出现了编号这样容易让小把戏穿帮的内容,可能需要考虑用 transport 定制滚动效果。

以上就是Vue3实现虚拟列表的示例代码的详细内容,更多关于Vue3虚拟列表的资料请关注脚本之家其它相关文章!

相关文章

  • VUE 组件的计算属性详解

    VUE 组件的计算属性详解

    这篇文章主要介绍了VUE组件的计算属性详解,使用计算机属性还是methods取决于你是否需要缓存,当遍历大数组和做大量计算时,应当使用计算机属性,除非你不希望得到缓存,下文来了解具体详情
    2022-06-06
  • Vue 详解mixins混入用法大全

    Vue 详解mixins混入用法大全

    如果我们在每个组件中去重复定义这些属性和方法会使得项目出现代码冗余并提高了维护难度,针对这种情况官方提供了Mixins特性,这时使用Vue mixins混入有很大好处,下面就介绍下Vue mixins混入使用方法,需要的朋友参考下吧
    2021-08-08
  • 解决elementUI中el-tree树形结构中节点过滤的问题

    解决elementUI中el-tree树形结构中节点过滤的问题

    这篇文章主要介绍了解决elementUI中el-tree树形结构中节点过滤的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • 完美解决element-ui的el-input设置number类型后的相关问题

    完美解决element-ui的el-input设置number类型后的相关问题

    这篇文章主要介绍了完美解决element-ui的el-input设置number类型后的相关问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-10-10
  • 详解Element 指令clickoutside源码分析

    详解Element 指令clickoutside源码分析

    这篇文章主要介绍了详解Element 指令clickoutside源码分析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-02-02
  • Element中table组件(el-table)右侧滚动条空白占位处理

    Element中table组件(el-table)右侧滚动条空白占位处理

    当我设置了max-height,就会在表格右侧出现一列空白的占位,本文主要介绍了Element中table组件(el-table)右侧滚动条空白占位处理,感兴趣的可以了解一下
    2023-09-09
  • Vue3中echarts无法缩放的问题及解决方案

    Vue3中echarts无法缩放的问题及解决方案

    很多朋友在使用vue3+echarts5技术时会遇到echarts无法绽放的问题,今天小编就给大家分享下问题描述及解决方案,感兴趣的朋友跟随小编一起看看吧
    2022-11-11
  • 少女风vue组件库的制作全过程

    少女风vue组件库的制作全过程

    这篇文章主要给大家介绍了关于少女风vue组件库的制作全过程,文中通过示例代码介绍的非常详细,对大家学习或者使用vue具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • Vue.js仿微信聊天窗口展示组件功能

    Vue.js仿微信聊天窗口展示组件功能

    这篇文章主要介绍了Vue.js仿微信聊天窗口展示组件功能,需要的朋友可以参考下
    2017-08-08
  • 在Vue3中使用BabylonJs开发 3D的初体验

    在Vue3中使用BabylonJs开发 3D的初体验

    这篇文章主要介绍了在 Vue3 中使用 BabylonJs 开发 3D 是什么体验,在本文中,向您展示了如何创建 Vue 组件、Babylon 类、在画布上渲染场景以及创建 3D 网格,需要的朋友可以参考下
    2022-07-07

最新评论