虚拟列表 - Vue3实现一个可动态改变高度的虚拟滚动列表

2023-09-20 14:40:56

虚拟列表 - Vue3实现一个可动态改变高度的虚拟滚动列表

前言

在开发中经常遇到大量的渲染列表数据问题,往往我们就只是简单地遍历渲染,没有过多地去关注是否会存在性能问题,这导致如果数据量较大的时候,比如上万条数据,将会在dom中渲染上万个节点,这将加大浏览器的开销,可能会导致页面卡顿,加载慢等性能问题。因此,在渲染大量数据时,可以选择使用虚拟列表,只渲染用户可视区域内的dom节点。该组件已开源上传npm,可以直接安装使用,Git地址在文尾。

虚拟列表实现原理

每条固定高度

1、通过传入组件的每条数据的高度,计算整个列表的高度,从而得到滚动列表的总高,并将总高赋值给列表。
2、监听滚动事件,监听外层容器的滚动事件,并确定可视区域内起止数据在总数据的索引值,这可以通过scrollTop来实现。
3、设置数据对应的元素,为每条数据设置一个绝对定位,其中top等于索引值乘以每条数据的高度。
4、考虑缓冲条数,为了避免滑动过快产生空白,可以设置缓冲条数。具体来说,如果滚动到底部,可以只显示最后N条数据,如果滚动到上部,可以只显示前N条数据。
这样,就可以实现一个固定高度的虚拟列表。

每条动态高度

原理和固定高度基本一致,差别在于,用户可以预先定义每条数据的高度,在渲染时再动态获取每一条数据的实际高度,从而重新计算滚动列表的总体高度。

主要代码实现

模板部分

showItemList循环可视区域内的数据+缓存区的数据

<template>
    <div class="virtual-wrap" ref="virtualWrap" :style="{
        width: width + 'px',
        height: height + 'px',
    }" @scroll="scrollHandle">
        <div class="virtual-content" :style="{height: totalEstimatedHeight +'px'}">
            <list-item v-for="(item,index) in showItemList" :key="item.dataIndex+index" :index="item.dataIndex" :data="item.data" :style="item.style"
                @onSizeChange="sizeChangeHandle">
                <template #slot-scope="slotProps">
                <slot name="slot-scope" :slotProps="slotProps"></slot>
                </template>
            </list-item>
        </div>
    </div>
</template>
获取需要渲染的数据

通过可视区域内的开始和结束索引,获取需要渲染的列表数据。

const getCurrentChildren = () => {
    //重新计算高度
    estimatedHeight(props.itemEstimatedSize,props.itemCount)
    const [startIndex, endIndex] = getRangeToRender(props, scrollOffset.value)
    const items = [];
    for (let i = startIndex; i <= endIndex; i++) {
        const item = getItemMetaData(i);
        const itemStyle = {
            position: 'absolute',
            height: item.size + 'px',
            width: '100%',
            top: item.offset + 'px',
        };
        items.push({
            style: itemStyle,
            data: props.data[i],
            dataIndex:i
        });
    }
    showItemList.value = items;
}
获取开始和结束索引
const getRangeToRender = (props: any, scrollOffset: any) => {
    const { itemCount } = props;
    const startIndex = getStartIndex(props, scrollOffset);
    const endIndex = getEndIndex(props, startIndex + props.buffCount);
    return [
        Math.max(0, startIndex -1),
        Math.min(itemCount - 1, endIndex ),
    ];
};

const getStartIndex = (props: any, scrollOffset: number) => {
    const { itemCount } = props;
    let index = 0;
    while (true) {
        const currentOffset = getItemMetaData(index).offset;
        if (currentOffset >= scrollOffset) return index;
        if (index >= itemCount) return itemCount;
        index++
    }
}

const getEndIndex = (props: any, startIndex: number) => {
    const { height, itemCount } = props;
    // 获取可视区内开始的项
    const startItem = getItemMetaData(startIndex);
    // 可视区内最大的offset值
    const maxOffset = Number(startItem.offset) + Number(height);
    // 开始项的下一项的offset,之后不断累加此offset,知道等于或超过最大offset,就是找到结束索引了
    let offset = Number(startItem.offset) + startItem.size;
    // 结束索引
    let endIndex = startIndex;

    // 累加offset
    while (offset <= maxOffset && endIndex < (itemCount - 1)) {
        endIndex++;
        const currentItem = getItemMetaData(endIndex);
        offset += currentItem.size;
    }
     // 更新已计算的项的索引值
    measuredData.lastMeasuredItemIndex = endIndex;
    return endIndex;
};
动态计算节点高度

const estimatedHeight = (defaultEstimatedItemSize = 50, itemCount: number) => {
    let measuredHeight = 0;
    const { measuredDataMap, lastMeasuredItemIndex } = measuredData;
    // 计算已经获取过真实高度的项的高度之和
    if (lastMeasuredItemIndex >= 0) {
        const lastMeasuredItem = measuredDataMap[lastMeasuredItemIndex];
        measuredHeight = lastMeasuredItem.offset + lastMeasuredItem.size;
    }
    // 未计算过真实高度的项数
    const unMeasuredItemsCount = itemCount - measuredData.lastMeasuredItemIndex - 1;
    // 预测总高度
    totalEstimatedHeight.value = measuredHeight + unMeasuredItemsCount * defaultEstimatedItemSize;
}

子组件实现

1、通过ResizeObserver在子节点高度变化时触发父组件的方法,重新计算整体高度。
2、通过插槽将每条数据动态插入到列表中。

<template>
    <div :style="style" ref="domRef">
        <slot name="slot-scope" :data="data"></slot>
    </div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'

const emit = defineEmits(['onSizeChange']);

const props = defineProps({
    style: {
        type: Object,
        default: () => { }
    },
    data: {
        type: Object,
        default: () => { }
    },
    index: {
        type: Number,
        default: 0
    }
})

const domRef = ref<any>(null);
const resizeObserver:any = null;


onMounted(() => {
    const domNode = domRef.value.children[0];
    emit("onSizeChange", props.index, domNode);
    const resizeObserver = new ResizeObserver(() => {
        emit("onSizeChange", props.index, domNode);
    });
    resizeObserver.observe(domNode);
})

onUnmounted(() => {
    if (resizeObserver) {
        resizeObserver?.unobserve(domRef.value.children[0]);
    }
})
</script>

组件使用

npm install @fcli/vue-virtually-list --save-dev 来安装

在项目中使用
import VueVirtuallyList from '@fcli/vue-virtually-list';
const app=createApp(App)
app.use(VueVirtuallyList);

示例:


<div class="content">
  <vue-virtually-list :data="list" :height="400" :width="600" :itemCount="1000" :itemEstimatedSize="20" :buffCount="50">
    <template #slot-scope="{slotProps}">
      <div class="li">{{ slotProps.data.text }}</div>
    </template>
  </vue-virtually-list>
</div>

属性属性名称类型可选值
data列表数据Array[]
height虚拟容器的高度number0
width虚拟容器的宽度number0
itemCount滚动列表的条数number0
itemEstimatedSize预设每行数据的高度number可不填,组件会动态计算
buffCount上下缓冲区的条数number增加快速滚动时的流畅性
#slot-scope插槽 | object | slotProps.data|
slot

例:

  <template #slot-scope="{slotProps}">
    <div class="li">{{ slotProps.data.text }}</div>
  </template>

Git地址:https://gitee.com/fcli/vue-virtually-list.git

更多推荐

移动端适配以及多屏幕自适应方案

文章目录前言一、移动端适配问题二、meta-viewport标记三、rem字体适配四、vw和vh五、postcss转换插件总结前言本文主要记录适配移动端以及多屏幕的解决办法,还有postcss转换插件的编写。一、移动端适配问题在MDN中提到:在移动设备和其他窄屏设备中,某些内容在比普通屏幕更宽的虚拟窗口或视口中渲染页面

PC微信3.9.7内测版,更新功能一览(附下载)

之前小编发布了PC微信3.9.7的内测版本,不过大家没有内测权限,不能够安装体验,本次正式版终于来了,大家可以下载安装体验,和之前一样小编给大家介绍本次PC版微信更新的内容,感兴趣的朋友可以下载体验一下!1、聊天界面表情弹窗新增搜索表情功能大家比较期待的表情搜索功能终于上线了,大家以后聊天终于可以使用更加丰富的表情包了

C语言零基础教程(memset,memcpy函数,memmove函数)

文章目录前言一、memset函数二、memcpy函数三、memmove函数总结前言本篇文章来讲解一下memset和memcpy函数,这两个函数在C语言中也是比较重要的,这里我们就来学习一下这两个函数的使用方法吧。一、memset函数memset函数是一个C标准库中的函数,用于将一块内存区域的每个字节设置为指定的值。me

双向链表的实现(增删查改)——最好理解的链表

双向链表的实现一,双向链表的特点二,双向链表的结构三,双向链表的内容实现3.1创建node节点3.2初始化3.3打印3.4插入3.4.1尾插3.4.2头插3.4.3在pos位置上插入3.5删除3.5.1尾删3.5.2头删3.5.3删除pos位置上的数据四,调试技巧(具体示例)五,总结一,双向链表的特点这里的双向链表就是

Python语言学习实战-内置函数reduce()的使用(附源码和实现效果)

实现功能reduce()是一个内置函数,它用于对一个可迭代对象中的元素进行累积操作。它接受一个函数和一个可迭代对象作为参数,并返回一个单个的累积结果。reduce()函数的语法如下:reduce(function,iterable[,initializer])其中,function是一个二元函数,它接受两个参数并返回一

Mysql的基本查询练习

目录一、Create1.1单行数据+全列插入1.2多行数据+指定列插入1.3插入否则更新1.4替换二、Retrieve2.1全列查询2.2指定列查询2.3查询字段为表达式2.4为查询结果指定别名2.5结果去重2.6where条件2.6NULL的查询2.7结果排序三、Update四、Delete五、插入查询结果六、聚合函

学科知识图谱学习平台项目 :技术栈Java、Neo4j、MySQL等超详细教学

项目设计集合(人工智能方向):助力新人快速实战掌握技能、自主完成项目设计升级,提升自身的硬实力(不仅限NLP、知识图谱、计算机视觉等领域):汇总有意义的项目设计集合,助力新人快速实战掌握技能,助力用户更好利用CSDN平台,自主完成项目设计升级,提升自身的硬实力。专栏订阅:项目大全提升自身的硬实力[专栏详细介绍:项目设计

ARMv7处理器

本文档介绍常见的ARM架构,包括Cortex-A5,Cortex-A7,Cortex-A8,Cortex-A9,Cortex-A15.常见的术语DFT(DesignforTest),为了增强芯片可测性而采用的一种设计方法APB(AdvancedPeripheralBus),是一种低速外设总线接口,通常用于将外部设备(如

2.策略模式

UML图代码main.cpp#include"Strategy.h"#include"Context.h"voidtest(){Context*pContext=nullptr;/*StrategyA*/pContext=newContext(newStrategyA());pContext->contextInter

如何理解JavaScript定时器的4种写法-附带面试题讲解

在JavaScript里,我们已经会使用一些原生提供的方法来实现需要延时执行的操作代码,比如很多在线时钟的制作,图片轮播的实现,还有一些广告弹窗,但凡可以自动执行的东西,都是可以和定时器有关的。今天就来和大家分享一下,关于我们在JavaScript里经常会使用到的定时器方法在JavaScript里,我们要学习四个定时器

基于GBDT+Tkinter+穷举法按排队时间预测最优路径的智能导航推荐系统——机器学习算法应用(含Python工程源码)+数据集(三)

目录前言总体设计系统整体结构图系统流程图运行环境Python环境Pycharm环境Scikit-learnt模块实现1.数据预处理2.客流预测3.百度地图API调用4.GUI界面设计1)手绘地图导入2)下拉菜单设计3)复选框设计4)最短路径结果输出界面设计5)智能推荐结果输出设计6)界面展示5.路径规划6.智能推荐相关

热文推荐