在大数据加载时,如果数据量过大,就会导致界面卡顿,导致用户流失。那就需要使用相关的处理方法,在开发中常用的方法就是利用虚拟滚动来处理界面的相关渲染。
在虚拟滚动时,只需要实现视图显示范围内的数据及时切换变化。
1.设计出相关的界面框架结构
<template>
<div ref="container" class="container" @scroll="handleScroll">
<div class="placeholder" :style="{ height: listHeight }"></div>
<div class="list-wrapper" :style="{ transform: getTransform }">
<div class="card-item" v-for="(item, index) in virtualData" ref="itemRefs" :key="index"
:data-index="item.index">
<span>{{ `${item.index}.${item.value}` }}</span>
</div>
</div>
</div>
</template>
<style>
.container {
height: 100%;
overflow: auto;
position: relative;
}
.placeholder {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.card-item {
padding: 10px;
color: #777;
box-sizing: border-box;
border-bottom: 1px solid #e1e1e1;
}
</style>
- container:视图显示区域
- placeholder:设为position:absolute,目的是设置滑动区域的高
- list-wrapper:内容填充区域
2. 设置完相关的的区域后,计算出需要滑动区域的高
根据传过来的数组的长度,算出最后一个数组的bottom位置,立即滑动区域的高
const listHeight = computed(() => {
const data = positions.value[positions.value.length - 1]?.bottom || 0
return `${data}px`
})
3. 通过滑动计算出显示区域的开始位置和结束位置
3.1 采用二分法快速算出滑动的开始位置
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
start.value = getStart(scrollTop)
offset.value = positions.value[start.value].top
}
const getStart = (scrollTop) => {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (positions.value[mid].bottom === scrollTop) {
return mid + 1;
} else if (positions.value[mid].bottom > scrollTop) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
3.2 在界面渲染后,获取滑动区域的高度
onMounted(() => {
containerHeight.value = container.value.clientHeight
})
3.3 将滑动区域的高/item的高,加上开始位置,就是结束的位置
const renderCount = computed(() => {
return Math.ceil(containerHeight.value / props.itemHeight)
})
const end = computed(() => {
return start.value + renderCount.value
})
4. 最关键的一步了,就是在每一行的高不确定时,应该如何计算呢?
4.1 在计算量较大的时候,引入worker,多线程进行,计算相关的数据
mainWorker.js
// 初始化计算position的值
function initPosition(data,height){
const positions = [];
for (let i = 0; i < data.length; i++) {
positions.push({
index: i,
height: height,
top: height * i,
bottom: height * (i + 1)
})
}
return positions;
}
addEventListener('message', e => {
const { data } = e
const obj = JSON.parse(data)
let postData = "";
if (obj?.type === 'initPosition') {
const positions = initPosition(obj.data, obj.height);
postData = JSON.stringify({
type: obj.type,
data: positions
})
}
return postMessage(postData);
})
const worker = ref(null)
worker.value = new Worker(new URL('../workers/mainWorker.js', import.meta.url))
const initPosition = () => {
positions.value = []
if (props.dataList.length > 0) {
sendMessageToWorker({
type: "initPosition",
data: props.dataList,
height: props.itemHeight
}, (data) => {
positions.value.push(...data.data)
})
}
}
4.2 传过来的数据偶尔还会有变化,利用watch监听数据变化
watch(() => props.dataList, () => {
initPosition()
}, {
immediate: true
})
5.每一个item的高不固定,在item渲染完成后,需要更新对应地方的bottom,更新内容的Y位置
5.1 使用onUpdate方法。onUpdate方法是在视图渲染完后成,就会自动执行。 onUpdate方法
const updatePosition = () => {
if (positions.value.length === 0) {
return
}
itemRefs.value.forEach((el) => {
const index = el.getAttribute('data-index')
const realHeight = el.getBoundingClientRect().height
let diffVal = positions.value[index]?.height - realHeight
const curItem = positions.value[index]
if (diffVal !== 0 && index < props.dataList.length) {
curItem.height = realHeight
curItem.bottom = curItem.bottom - diffVal
for (let i = index + 1; i < positions.value.length - 1; i++) {
positions.value[i].top = positions.value[i].top - diffVal;
positions.value[i].bottom = positions.value[i].bottom - diffVal;
}
}
})
}
onUpdated(() => {
updatePosition()
})
5.2 根据滑动的高,得出的位置,进行translateY的转换
const getTransform = computed(() => {
return `translate3d(0,${offset.value}px,0)`
})
6.VirutalItem的完整代码
<template>
<div ref="container" class="container" @scroll="handleScroll">
<div class="placeholder" :style="{ height: listHeight }"></div>
<div class="list-wrapper" :style="{ transform: getTransform }">
<div class="card-item" v-for="(item, index) in virtualData" ref="itemRefs" :key="index"
:data-index="item.index">
<span>{{ `${item.index}.${item.value}` }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated, defineProps } from 'vue'
const props = defineProps({
dataList: {
type: Array,
default: () => {
return []
}
},
itemHeight: {
type: Number,
default: 60
}
})
const container = ref(null)
const containerHeight = ref(0)
const start = ref(0)
const offset = ref(0)
const itemRefs = ref()
const positions = ref([])
const worker = ref(null)
worker.value = new Worker(new URL('../workers/mainWorker.js', import.meta.url))
function sendMessageToWorker(data, cb) {
const dataString = JSON.stringify(data)
worker.value.postMessage(dataString)
worker.value.onmessage = (e) => {
const obj = JSON.parse(e.data)
if(obj.type === data.type){
cb(obj)
}
}
}
const listHeight = computed(() => {
const data = positions.value[positions.value.length - 1]?.bottom || 0
return `${data}px`
})
const getTransform = computed(() => {
return `translate3d(0,${offset.value}px,0)`
})
const renderCount = computed(() => {
return Math.ceil(containerHeight.value / props.itemHeight)
})
const end = computed(() => {
return start.value + renderCount.value
})
const virtualData = computed(() => {
return props.dataList.slice(start.value, end.value)
})
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
start.value = getStart(scrollTop)
offset.value = positions.value[start.value].top
}
const getStart = (scrollTop) => {
let left = 0;
let right = positions.value.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (positions.value[mid].bottom === scrollTop) {
return mid + 1;
} else if (positions.value[mid].bottom > scrollTop) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
const initPosition = () => {
positions.value = []
if (props.dataList.length > 0) {
sendMessageToWorker({
type: "initPosition",
data: props.dataList,
height: props.itemHeight
}, (data) => {
positions.value.push(...data.data)
})
}
}
const updatePosition = () => {
if (positions.value.length === 0) {
return
}
itemRefs.value.forEach((el) => {
const index = el.getAttribute('data-index')
const realHeight = el.getBoundingClientRect().height
let diffVal = positions.value[index]?.height - realHeight
const curItem = positions.value[index]
if (diffVal !== 0 && index < props.dataList.length) {
curItem.height = realHeight
curItem.bottom = curItem.bottom - diffVal
for (let i = index + 1; i < positions.value.length - 1; i++) {
positions.value[i].top = positions.value[i].top - diffVal;
positions.value[i].bottom = positions.value[i].bottom - diffVal;
}
}
})
}
onMounted(() => {
containerHeight.value = container.value.clientHeight
})
onUpdated(() => {
updatePosition()
})
watch(() => props.dataList, () => {
initPosition()
}, {
immediate: true
})
</script>
<style>
.container {
height: 100%;
overflow: auto;
position: relative;
}
.placeholder {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.card-item {
padding: 10px;
color: #777;
box-sizing: border-box;
border-bottom: 1px solid #e1e1e1;
}
</style>
7.在外层使用时设置对应div的宽高
<template>
<div class="virtual-container">
<VirutalItem :dataList="data"></VirutalItem>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { testData } from "../utils/testData.js"
import VirutalItem from './VirutalItem.vue'
const data = ref([])
const index = ref(0)
// 测试数据
for (let i = 0; i < 10000; i++) {
data.value.push({
index: i + 1,
value: testData[index.value]
})
index.value++
if (index.value >= testData.length) {
index.value = 0
}
}
</script>
<style>
.virtual-container {
width: 400px;
height: 800px;
}
</style>
8.相关的学习链接暂时找不到了,未加上,等以后找到了,再补。。。。