以下代码原理是使用requestAnimationFrame(callback) 方法
Test01.vue
<template>
<div class="container">
<div v-for="n in 100" :key="n">
<HeavyCom v-if="defer(n)"></HeavyCom>
</div>
</div>
</template>
<script>
import HeavyCom from "@/views/HeavyCom";
export default {
components: { HeavyCom },
data() {
return {
frameCount: 0,
};
},
created() {
this.updateFrameCount();
},
methods: {
updateFrameCount() {
const maxCount = 100;
this.frameCount++;
if (this.frameCount < maxCount) {
requestAnimationFrame(this.updateFrameCount);
}
},
defer(n) {
return this.frameCount >= n;
},
},
};
</script>
<style>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}
</style>
子组件HeavyCom.vue
<template>
<div class="item-container">
<div class="item" v-for="n in 5000" :key="n"></div>
</div>
</template>
<script>
export default {
name: "HeavyCom"
}
</script>
<style lang="scss">
.item-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
border: 3px solid #f40;
width: 600px;
height: 600px;
box-sizing: border-box;
.item {
width: 4px;
height: 4px;
background: #ccc;
margin: 1px;
}
}
</style>
Test01.vue
<template>
<div class="container">
<div v-for="n in 100" :key="n">
<HeavyComp v-if="defer(n)"></HeavyComp>
</div>
</div>
</template>
<script setup>
import HeavyComp from "@/views/HeavyComp";
import { useDefer } from './useDefer'
const defer = useDefer();
</script>
<style>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}
</style>
子组件HeavyComp.vue
<template>
<div class="item-container">
<div class="item" v-for="n in 5000" :key="n"></div>
</div>
</template>
<style>
.item-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
border: 3px solid #f40;
}
.item {
width: 5px;
height: 3px;
background: #ccc;
margin: 0.1em;
}
</style>
useDefer.js
import {ref, onUnmounted} from "vue";
export function useDefer(maxCount = 100) {
const frameCount = ref(0);
let rafId;
function updateFrameCount() {
rafId = requestAnimationFrame(() => {
frameCount.value++
if (frameCount.value >= maxCount) {
return;
}
updateFrameCount();
});
}
updateFrameCount();
onUnmounted(() => {
cancelAnimationFrame(rafId);
});
return function defer(n) {
console.log(frameCount.value, n);
return frameCount.value >= n;
}
}
结果:这里为了录制gif不卡顿,只渲染50个重组件
优点: 首屏渲染很快,后续的再依次加载
缺点: 快速滚动时,会出现短暂的白屏
通过这张图来表示虚拟列表,红框代表你的手机,黑条代表一条条数据
思路:我们只要知道手机屏幕最多能放下几条数据,当下拉滑动时,通过双指针的方式截取相应的数据就可以了。
🚩 PS:为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。
在Test02.vue
<template>
<div ref="list" class="v-scroll" @scroll="scrollEvent($event)">
<div class="infinite-list" :style="{ height: listHeight + 'px' }"></div>
<div class="scroll-list" :style="{ transform: getTransform }">
<div ref="items" class="scroll-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemHeight + 'px',lineHeight: itemHeight + 'px' }">{{ item.msg }}</div>
</div>
</div>
</template>
<script>
export default {
name: "Test02",
data() {
return {
// 数据
listData: [],
// 每项的高度
itemHeight: 60,
//可视区域高度
screenHeight: 600,
//偏移量
startOffset: 0,
//起始索引
start: 0,
//结束索引
end: null,
};
},
computed: {
//列表总高度
listHeight() {
return this.listData.length * this.itemHeight;
},
//可显示的列表项数
visibleCount() {
return Math.ceil(this.screenHeight / this.itemHeight);
},
//偏移量对应的style
getTransform() {
return `translate(0,${this.startOffset}px)`;
},
//获取真实显示列表数据
visibleData() {
return this.listData.slice(this.start, Math.min(this.end, this.listData.length));
}
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemHeight);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemHeight);
}
},
mounted() {
for (let i = 1; i <= 100000; i++) {
this.listData.push({id: i, msg: i})
}
this.start = 0;
this.end = this.start + this.visibleCount;
}
}
</script>
<style>
.v-scroll {
height: 600px;
width: 400px;
border: 3px solid #000;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.scroll-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.scroll-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>
结果:这里为了录制gif不卡顿,只渲染一万条
优点:页面上只渲染出可视区域的内容,非常丝滑,无白屏现象