优化系列:Vue3.0实现一下虚拟列表(固定高度)

816 阅读3分钟

一. 前言

在现代前端的发展中,虚拟列表技术已经很常见了。市面上常见的一些框架(element、antd)等,虚拟列表技术已经很成熟。但是我们自己还是有必要去实现一下,了解一些前端的思想和概念。 最近一直在搞别的东西,前端都没怎么看,顺便复习一下。

二. 虚拟列表的概念

假如,一次性给你10w条数据,在一个滚动列表里面渲染出来。你会考虑怎么办?摆烂?还是摆烂?

image.png

没关系,我有虚拟列表。

1. 基本概念

什么是虚拟列表?虚拟列表就是是确保性能的前提下,利用一定的技术模拟全数据一次性渲染后效果。(后端不分段数据,我来干喽!)图自己不想画了,随便找了一张。

image.png

整个虚拟列表划分为三个区域

  • 是上缓冲区
  • 可视区
  • 下缓冲区

当用户进行操作的时候滚动到一个元素离开可视区范围内时,就去掉上缓冲区顶上的一个元素,然后再下缓冲区增加一个元素。依此类推。

allDataList就是用户的view区域,这里是dom最终渲染区域。

2. 分析

根据上面我们推断出来实现的几个步骤:

这里只考虑一下子元素固定高度的列表。

  • 计算出用户可视区域的容器高度
  • 计算出一个大容器(内层滚动容器)高度
  • 监听滚动事件根据滚动位置动态改变可视列表

ok,那我们实现一下。

三. 虚拟列表的实现

新建一个Vue3.0项目。可以参考我之前的文章 如何从无到有搭建一套完整的低代码平台(一)项目搭建 这里就不过多描述了。

在src下新建FixedHeightList文件夹。这个文件夹就作为我们开发的目录。

1. api设计

没什么好说的,按照下面的来,目前先扩展那么多api。有需要再加。

image.png

那我们按照这个api一步一步来实现就好。

2. hooks实现

在FixedHeightList下新建一个useFixedHeightListHooks.ts:

先上代码,然后一点一点解释

import { ref, Ref, onMounted } from 'vue';
import { IlistOptionsProps, IflowContainerStyle, IoutConainerStyle, IrenderItem } from './typings';

type fixedHeightListType = {
    flowContainerStyle: Ref<IflowContainerStyle>,
    outConainerStyle: Ref<IoutConainerStyle>,
    scrollHandler: (e: Event) => void,
    renderItem: Ref<IrenderItem[]>
}


export default function useFixedHeightListHooks(props: IlistOptionsProps): fixedHeightListType {
    const { height, width, totalListCount, listItemSize } = props;

    const scrollOffset = ref<number>(0);
    const renderItem = ref<IrenderItem[]>([]);

    const getCurrentRenderItem = (): IrenderItem[] => {
        // 开始renderItem的位置(也就是每个item的索引)
        const startIndex = Math.floor(scrollOffset.value / listItemSize);
        // 上缓冲区的开始索引 用startIndex - 2 (默认上缓冲区的item数量为2)
        const topStartIndex = Math.max(0, startIndex - 2);
        // 可视区域的最大item数量
        const visibleCount = Math.ceil(Number(height.includes('px') ? height.replace('px','') : height) / listItemSize);
        // 下缓冲区结束索引
        const bottomEndIndex = Math.min(totalListCount - 1, startIndex + visibleCount + 2);

        const items = [] as IrenderItem[];
        for (let i = topStartIndex; i < bottomEndIndex ; i++) {
            const itemStyle = {
                position: 'absolute',
                height: listItemSize + 'px',
                width: '100%',
                top: `${listItemSize * i}px`,
                'background-color': i % 2 ? 'blue' : 'pink'
            }
            items.push({
                key: i,
                label: i,
                style: itemStyle
            })
        }

        return items;
    }

    // 滚动容器的高度(实际渲染容器)
    const flowContainerStyle = ref<IflowContainerStyle>({
        position: 'relative',
        width,
        height,
        overflow: 'auto'
    })

    // 外部容器的高度(放置全部子元素)通过itemSize撑起来的元素
    const outConainerStyle = ref<IoutConainerStyle>({
        height: `${listItemSize * totalListCount}px`,
        width: '100%'
    })

    function scrollHandler(e: Event) {
       const { scrollTop } = <HTMLDivElement>e.currentTarget;
       scrollOffset.value = scrollTop;
       renderItem.value = getCurrentRenderItem();
    }

    onMounted(() => {
        renderItem.value = getCurrentRenderItem();
    })

    return {
        flowContainerStyle,
        outConainerStyle,
        renderItem,
        scrollHandler
    }
}

完成之后,我们在项目中使用一下。新建一个FixedHeightList/index.vue

<template>
    <div :style="flowContainerStyle" @scroll="scrollHandler">
        <div :style="outConainerStyle">
            <div v-for="item in renderItem" :key="item.key" :style="item.style">
                {{ item.label }}
            </div>
        </div>      
    </div>
</template>

<script lang="ts" setup>
import { toRefs } from 'vue';
import useFixedHeightListHooks from './useFixedHeightListHooks';

export interface IlistOptionsProps {
    totalListCount: number,
    listItemSize: number,
    width: string,
    height: string
}

const props = withDefaults(defineProps<IlistOptionsProps>(), {
    totalListCount: 1000,
    listItemSize: 50,
    width: '200px',
    height: '400px'
})

const { flowContainerStyle, outConainerStyle, scrollHandler, renderItem } = toRefs(useFixedHeightListHooks(props))

</script>

外层容器内层容器的样式暴露出去,然后在style上使用一下。通过监听scroll,完成整个流程。最终的效果如下:

image.png

不管我们如何滚动,最终在页面上渲染的dom数量是保持不变的!

3. typings代码

最后把ts的类型放一下,供大家参考。 在FixedHeightList文件夹下新建typings.d.ts

/// <reference types="vite/client" />

export interface IlistOptionsProps {
    totalListCount: number,
    listItemSize: number,
    width: string,
    height: string
}

export type IflowContainerStyle = {
    position: string,
    width: string,
    height: string,
    overflow: 'auto' | 'scroll' | 'hiddlen',
    [key: string]: any
}

export type IoutConainerStyle = Pick<IflowContainerStyle, 'width' | 'height'>

export type IrenderItem = {
    key: number,
    label: number,
    style: Pick<IflowContainerStyle, 'width' | 'height' | 'position', | 'top'>
}