虚拟列表(固定高度)
虚拟列表更复杂,并且如果不用transform而是用top的话会有大量的回流,很容易导致白屏,但是用户滚动一次就能浏览所有内容,浏览体验是连续的,更好
浏览器的原生列表也能滚动,但是他所有的dom都是实际存在的,也就是假如有1000条数据,就会渲染1000个dom计算布局什么的,而虚拟列表只取指定个数的dom渲染,其他的滚动到的时候再渲染
原理: 虚拟列表外层是一个容器作为滚动容器(也就是可视窗口),容器有两个子元素,第一个子元素是一个占位元素,高度为所有数据的高度,用来撑开进度条,即让进度条看起来合理,第二个子元素是真正要显示数据的元素的数据容器,高度为可视窗口的高度,用绝对定位固定在可视窗口上,当用户滚动的时候只改变数据容器里面的数据,dom是复用的
过程:
- 根据滚动的高度计算当前开头应该在的位置的数据索引,将该数据索引与0比较取较大值防止得到负数位置
- 根据开始位置与可视窗口高度计算结尾应该在的位置的数据索引,将该数据索引与数组长度比较取较小值防止超过数组长度
- 根据起始索引和结束索引切割数组得到对应的子数组渲染在数据容器中
实现:
<script setup>
import { computed, onMounted, ref, watch } from "vue";
//初始化工作
const defaultArr = new Array(100).fill(0).map((_, i) => i);
const container = ref(null);
let startIndex = ref(0);
let endIndex = ref(0);
let itemHeight = 200;//单个子元素的高度
let contentHeight = itemHeight * defaultArr.length;//虚拟区总高度
let showCount = 0;
const bufferRation = 2;
//节流
const throlle = (func, delay) => {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime > delay) {
func.apply(this, args);
lastTime = now;
}
}
}
//滚动逻辑
const handelScroll = throlle(() => {
startIndex.value = Math.floor(container.value.scrollTop / itemHeight);//计算开始索引
endIndex.value = startIndex.value + showCount;//计算结束索引
console.log(startIndex.value);
console.log(endIndex.value);
}, 50);
const visibleItems = computed(() => {
const start = Math.max(0, startIndex.value - bufferRation); // 确保起始索引不小于 0
const end = Math.min(defaultArr.length, endIndex.value + bufferRation); // 确保结束索引不超过数组长度
return defaultArr.slice(start, end);
});
const offsetTop = computed(() => {
const adjustedStartIndex = Math.max(0, startIndex.value - bufferRation); // 确保不小于 0
return adjustedStartIndex * itemHeight;
});
onMounted(() => {
let containerHeight = container.value.clientHeight;//外层盒子高度
showCount = Math.ceil(containerHeight / itemHeight);//展示的个数
endIndex.value = startIndex.value + showCount;
})
</script>
<template>
<div class="container" ref="container" @scroll="handelScroll">
<div :style="{ height: contentHeight + 'px' }"> </div>
<div class=" content" :style="{ transform: `translateY(${offsetTop}px)` }">
<template v-for=" (item) in visibleItems" :key="item">
<div class="children-box" :style="{ height: `${itemHeight}px` }">
{{ item }}
</div>
</template>
</div>
</div>
</template>
<style scoped lang="scss">
.container {
height: 600px;
width: 200px;
background: rgb(255, 0, 0);
margin: 10px auto;
overflow: auto;
position: relative;
}
.content {
width: 100%;
position: absolute;
top: 0;
left: 0;
}
.children-box {
height: 200px;
background: pink;
border: 1px solid rgb(0, 0, 0);
}
</style>
优化:
- 用transform替代top,不回流和重绘,性能较好,注意这里的不回流并不是这个动作完全不回流,见2
- 虚拟列表鼠标滚动的时候实际上是,获取鼠标滚动位置->回流一次,这一次用什么都逃不掉,如果用删除添加dom那这个操作还会回流两次,现在优化为复用dom就不需要这两次回流了,如果用top会多出来每次让当前数据到顶部的回流,这组有几个就回流几次,而transform省掉的就是这部分
- 可以用节流来减少回流次数,因为鼠标滚动一次实际上就是很多帧的改动,每一帧都要回流,用节流可以让他不要每一帧都回流而是攒着等到指定的时间一起回流
- 注意:!!!! 用了transform后虚拟列表滚动时仍然会回流,这是因为虚拟列表有两个导致回流操作的事件,一个是获取鼠标的滚动位置,一个是修改列表的位置,transform避免了第二个的回流但是无法避免第一个的回流,所以需要节流来优化第一个的回流
- 用will-change提示transform修改
分页
用户需要点击下一页或者跳转页码,会打断浏览的连续性,并且把实际上是把一次请求分成多次请求,网络资源消耗多点,但是逻辑简单
后端分页: 后端通过limit语句分块返回,前端只需要展示当前页即可,大数据的情况下前端渲染压力小,内存占用小
前端分页: 前端可以建立dom池,每次只修改数据不重新渲染dom节点
单纯的数据展示可以用Canvas
实现: 直接在画布上画
优点: 内存只有一条Canvas的dom,渲染极快,占用内存极少
缺点: 开发复杂度高,所有交互逻辑都需要自己获取坐标去处理,适合不需要复杂交互逻辑的