一、什么是虚拟列表
在传统的列表渲染中,如果列表数据过多,一次性渲染所有数据将耗费大量的时间和内存。当我们上下滚动时,性能低的浏览器或电脑都会感觉到非常的卡,这对用户的体验时是致命的。
于是我们会想到懒加载,当资源到达可视窗口内时,继续向服务器发送请求获取接下来的资源,不过当获取的资源越来越多时,此时浏览器不断重绘与重排,这样的开销也是要考虑的当数量多到一定程度时,页面也会出现卡顿。
此时我们会想到虚拟列表,虚拟列表只渲染当前可见的部分数据,随着滚动条的滚动,只渲染当前可见的列表项,从而大大减少了渲染时间。同时支持无限滚动,用户只需要不停地滚动页面,就可以看到所有的数据,从而提高了用户的体验。
另外,需要注意的是,懒加载和虚拟列表也是有区别的:
二、虚拟列表的实现
盗用一张很经典的图,我们用数组保存要渲染的数据,同时监听鼠标滚动事件,获取滚动的距离,通过相关计算我们计算出滚动距离下对应的数组对应的渲染的开始索引和结束索引,用slice方法截取出来得到新数组,把新数组渲染到页面。以下是具体实现
三、代码讲解
(1)html和css
强调一下第三个div的作用(.list-view-shadow),高度设置为装下所有列表的总高度,触发滚动条的出现;ref="content"这个div就是渲染区域;其他的不多于讲解
{{ item }}
(2)核心代码
1)数据初始化,data用来存储传进来的数据,这里我们自己使用随机函数生成高度数据,数组computedData是辅助数组,里面存放到i元素(包括自身)的累加高度,后面滚动后参考比较滚动距离和累计高度(后面有),从而算出索引,再计算content(dom元素)该translate的距离
let scrollBox = ref(null).value;//获取dom元素
let content = ref(null).value;
let contentHeight = computed({
get() {
return computedData[computedData.length - 1].bom + "px";
},
});
onMounted(() => {
updateVisibleData();//首次渲染数据
});
let data = [];//存储随机高度,想要固定高度列表的童鞋把随机函数去掉即可
let minHeight = 35;//设置最小高度
for (let i = 0; i < 100; i++) {
//这里val相当于列表id啦
data.push({ val: i, height: minHeight + Math.random() * 20 });
}
let computedData = [];//辅助数组存储累加高度,用来修正高度,计算translate距离
for (let i = 0; i < data.length; i++) {
let bom = data[i].height + (i === 0 ? 0 : computedData[i - 1].bom);
computedData[i] = { val: data[i].val, bom };
}
//visibleData用来存储展示的数据
//start用来记录该渲染的数据的首个索引
let state = reactive({
visibleData: [],
start:0
});
2)实现更新渲染数组的函数(重点)。首先根据木桶效应计算出渲染列表容量,各位想想如果不用minHeight会出现什么效果(大家可以shishi);这个state.start是个响应式数据,后面滚动发生高度变化,二分法计算出首个索引,state.start变化时会触发这个更新函数(下面有);然后从原数组中截取需要渲染的数组,页面绑定了这个数组,所以视图会更新;
接下来计算滚动距离,累计到当前索引高度 - 当前索引所占高度,这就实现了ref为content这个div不偏移过头,确保里面列表计算出来的首个索引的元素可以进入视图中
function updateVisibleData() {
//计算最大容量(渲染呈现的区域 / 最小高度)
const visibleCount = Math.ceil(scrollBox.clientHeight / minHeight);
const end = state.start + visibleCount;
state.visibleData = data.slice(state.start, end);
//计算滚动距离(累计到当前索引高度 - 当前索引所占高度,这就实现了视图包含当前索引的那项div)
let scrollLen = computedData[state.start].bom - data[state.start].height;
//设置偏移距离(因为鼠标一直在滚动,不设置的话这个呈现列表就在原地不动啦)
content.style.webkitTransform = `translate3d(0, ${scrollLen}px, 0)`;
}
3)监听鼠标滚动事件,触发findStart函数,在findStart中,使用二分法寻找computedData[i].bom最先大于scrollTop的索引下标,为什么要是判断其底部是否大于scrollTop呢?因为如果滚动距离没有超过一个小列表的底部(累加)距离时(下图简称bom),我们这个列表应该还是可见的,不然页面不连贯 ,如下图
//用watch来监听start是否发生变化,如果发生变化才调用处理函数,这个可以提升性能
watch(()=>state.start,(newval,oldval)=>{
if(newval!==oldval){
updateVisibleData();
}
})
function handleScroll() {
const scrollTop = scrollBox.scrollTop;//获取滚动距离
state.start = findStart(scrollTop);
}
function findStart(scrollTop) {
let left = 0;
let right = computedData.length;
//二分法寻找start
while (left + 1 != right) {
let mid = Math.floor((left + right) / 2);
if (computedData[mid].bom > scrollTop) {
right = mid;
} else {
left = mid;
}
}
//为什么要返回right,因为我们1判断的是bom属性,按照上面的算法来说,right下标下面的bom属性永远大于滚动距离----
//left下标对于的bom永远小于等于滚动距离,也就是说left索引对应的元素可以不用渲染;如果滚动距离没超过某个索引下的bom属性时,当前索引有效(也就是right)。
return left === 0 ? 0 : right;
}
另外为了减少触发updateVisibleData函数的触发,需要设置watch监听器,当state.start也就是渲染数据的索引改变时,我们才调用updateVisibleData函数。
四、全部代码
{{ item.val }}--height--{{ item.height }}