一. 前言
在现代前端的发展中,虚拟列表技术已经很常见了。市面上常见的一些框架(element、antd)等,虚拟列表技术已经很成熟。但是我们自己还是有必要去实现一下,了解一些前端的思想和概念。 最近一直在搞别的东西,前端都没怎么看,顺便复习一下。
二. 虚拟列表的概念
假如,一次性给你10w条数据,在一个滚动列表里面渲染出来。你会考虑怎么办?摆烂?还是摆烂?
没关系,我有虚拟列表。
1. 基本概念
什么是虚拟列表?虚拟列表就是是确保性能的前提下,利用一定的技术模拟全数据一次性渲染
后效果。(后端不分段数据,我来干喽!)图自己不想画了,随便找了一张。
整个虚拟列表划分为三个区域
- 是上缓冲区
- 可视区
- 下缓冲区
当用户进行操作的时候滚动到一个元素离开可视区范围内时,就去掉上缓冲区顶上的一个元素,然后再下缓冲区增加一个元素。依此类推。
allDataList就是用户的view区域,这里是dom最终渲染区域。
2. 分析
根据上面我们推断出来实现的几个步骤:
这里只考虑一下子元素固定高度
的列表。
- 计算出用户可视区域的
容器高度
- 计算出一个大容器(
内层滚动容
器)高度 - 监听滚动事件根据滚动位置动态改变
可视列表
ok,那我们实现一下。
三. 虚拟列表的实现
新建一个Vue3.0项目。可以参考我之前的文章 如何从无到有搭建一套完整的低代码平台(一)项目搭建 这里就不过多描述了。
在src下新建FixedHeightList文件夹。这个文件夹就作为我们开发的目录。
1. api设计
没什么好说的,按照下面的来,目前先扩展那么多api。有需要再加。
那我们按照这个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
,完成整个流程。最终的效果如下:
不管我们如何滚动,最终在页面上渲染的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'>
}