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


    虚拟列表 - 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    获取需要渲染的数据

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    获取开始和结束索引
    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    动态计算节点高度
    
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    子组件实现

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

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    组件使用

    npm install @fcli/vue-virtually-list --save-dev 来安装
    
    在项目中使用
    import VueVirtuallyList from '@fcli/vue-virtually-list';
    const app=createApp(App)
    app.use(VueVirtuallyList);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    示例:

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

    例:

      
    
    • 1
    • 2
    • 3

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

  • 相关阅读:
    asp.net 学校资源信息管理系统VS开发sqlserver数据库web结构c#编程计算机网页项目
    zabbix设置企业微信预警+邮件告警
    C++多重继承解决方法
    寻找特殊年号
    网络参考模型与标准协议(一)
    传统社区如何进行数字化转型?快鲸智慧社区解决方案为你支招
    中国企业400电话在线申请办理
    栈和队列(数据结构、C语言)
    如何排查Java内存泄漏?
    找了很多关于抖音小店的干货文章,但还是做不好,这是为什么呢?
  • 原文地址:https://blog.csdn.net/qq_34185872/article/details/133079776