当遇到不能用分页方式来加载列表数据的业务情况,可以使用长列表,但是同时加载大量数据时性能消耗大,因此可以使用虚拟列表,只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染,从而达到较高的渲染性能
实现:
虚拟列表的实现,就是在首屏加载时,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除
1 | <div class="infinite-list-container"> |
infinite-list-container
为可视区域
的容器infinite-list-phantom
为容器内的占位,高度为总列表高度,用于形成滚动条infinite-list
为列表项的渲染区域
可以计算出:
列表总高度:listHeight = listData.length*itemSize
可显示列表项数:visibleCount
数据起始索引:startIndex = Math.floor(scrollTop/itemSize)
数据结束索引:endIndex = startIndex + visibleCount
列表显示数据visibleData = listData.slice(startIndex,endIndex)
滚动后渲染区域对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式将渲染区域便宜到可是区域中
偏移量startOffset = this.startIndex * this.itemSize
1
2
3
4
5
6
7
8
9
10
11
12
13
14<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63export default {
name:'VirtualList',
props: {
//所有列表数据
listData:{
type:Array,
default:()=>[]
},
//每项高度
itemSize: {
type: Number,
default:200
}
},
computed:{
//列表总高度
listHeight(){
return this.listData.length * this.itemSize;
},
//可显示的列表项数
visibleCount(){
return Math.ceil(this.screenHeight / this.itemSize)
},
//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
data() {
return {
//可视区域高度
screenHeight:0,
//偏移量
startOffset:0,
//起始索引
start:0,
//结束索引
end:null,
};
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize);
}
}
};
列表项动态高度
当列表中包含文本之类的可变内容时,会导致列表项的高度并不相同,在虚拟列表中应用动态高度的解决方案一般有三种:
1.对组件属性itemSize进行扩展,支持传递类型为数字,数组,函数
2.将列表项渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染到可视区域内
先渲染到屏幕外,再渲染到屏幕内,会导致渲染成本增加一倍,对于数百万用户在低端移动设备上使用的产品来说不切实际
3.以预估高度先行渲染,然后获取真实高度并缓存
使用第三种方法:positions用来存储列表项渲染后每一项高度以及位置信息:
1 | import { number } from "prop-types" |
列表的总高度等于列表项实际中最后一项的地步距离列表顶部的位置
getListHeight.ts:
1 | import { PositionType } from "./initPositionCache" |
更新缓存:在页面更新阶段,通过遍历列表每一项,获得实际内容撑起的高度height,和预估的高度oldHeight,如果两者存在差值,则更新该列表项的bottom和height,以及更新列表项后面每一项的top和bottom
1 | export const updateItemSize = ( |
滚动后获取列表的开始索引的方法修改为通过缓存获取:
1 | //获取列表起始索引 |
由于缓存数据是有顺序的,所以获取开始索引的方法可以考虑通过二分查找的方式降低检索次数:
1 | import { PositionType } from "./initPositionCache" |
滚动过程中二分查找到可视区域的内容
1 | //回调函数在浏览器下一次重绘之前执行 |
在每一次页面重新挂载后监听滚动事件,卸载后移除回调函数
1 | //useLayoutEffect相当于componentDidMount,在dom更新后,页面渲染前执行,避免出现闪烁现象 |
滚动后将偏移量的获取方式进行变更:
1 | if(this.start >= 1){ |
在componetDidUpdate阶段更新缓存
1 | //在componentDidUpdate阶段更新缓存,useEffect渲染到页面后异步执行 |
获取结束索引的函数:
1 | export const getEndIndex = (resource:Array<string>,startIndex: number,visibleCount: number) => { |
当滚动过快时,会出现短暂的白屏现象,为了使页面平滑滚动,我们还需要再可见区域的上方和下方渲染额外的项目,在滚动时基于一些缓冲,所以将屏幕分为三个区域:
可视区域上方:above
可视区域:screen
可视区域下方:below
定义bufferScale,用于接收缓冲区数据和可视区数据的比例:
1 | const bufferScale = 1 |
获取总偏移量getStartOffset:
1 | export const getStartOffset = (startIndex: number,positions: Array<PositionType> = [],aboveCount: number) => { |
完整源码:https://github.com/Coloey/july-design/tree/master/src/vitual-list
参考: