实现虚拟滚动功能可以帮助优化页面性能,尤其是在处理大量数据(如长列表或表格)时。虚拟滚动的核心思想是只渲染当前视口(Viewport)内可见的元素,而不是一次性渲染所有元素。这样可以显著减少DOM操作的开销,提升页面的加载速度和滚动性能。
以下是实现虚拟滚动的详细步骤和代码示例,我们将使用纯JavaScript来实现一个简单的虚拟滚动列表。
1. 计算视口大小和元素高度
视口大小:当前可视区域的高度。 元素高度:每个列表项的高度。
js
// 获取元素高度
const boxHeight = getPx(itemHeight.value) + getPx(marginBottom.value);
// 获取视口高度
nextTick(async () => {
const res = await getRect(".hy-virtual-container");
viewHeight.value = (res as UniApp.NodeInfo).height ?? 0;
});
2. 计算当前可视区域内的元素范围
起始索引:当前可视区域的第一个元素的索引。 结束索引:当前可视区域的最后一个元素的索引。
javascript
// 起始索引
const start = computed(() => {
const s = Math.floor(scrollTop.value / boxHeight);
return Math.max(0, s * line.value);
});
javascript
// 结束索引
const over = computed(() => {
const o = Math.floor(
(scrollTop.value + viewHeight.value + 1) / boxHeight + 5,
);
return Math.min(list.value.length, o * line.value);
});
4. 初始化渲染
截取需要展示的数据列表
javascript
const virtualData = computed(() => {
return list.value.slice(start.value, over.value);
});
5. 监听滚动事件
动态更新:当用户滚动时,根据滚动位置动态更新可视区域内的元素。
js
const onScroll = async (e: any) => {
scrollTop.value = e.detail.scrollTop ?? 0;
};
6.完整代码如下:
html
<template>
<scroll-view
ref="hyVirtualContainer"
@scroll="onScroll"
@scrolltolower="scrollToLower"
:lower-threshold="showDivider ? 40 : 10"
:scroll-y="true"
scroll-with-animation
class="hy-virtual-container"
>
<view class="hy-virtual-container__list">
<slot
v-if="slotDefault"
:record="line === 1 ? virtualData : waterfall"
></slot>
<template v-else>
<view
v-if="line === 1"
class="hy-virtual-container__list--item"
v-for="(item, i) in virtualData"
:key="typeof item === 'string' ? i : item[keyField]"
:style="itemStyle"
@click="handleClick(item)"
>
<slot style="height: 100%" name="content" :record="item"></slot>
</view>
<view
v-if="line === 2"
class="hy-virtual-container__list--left hy-virtual-container__list--box"
>
<view
v-if="slots.left"
class="hy-virtual-container__list--box-item"
v-for="item in waterfall.left"
:key="item[keyField]"
:style="itemStyle"
@click="handleClick(item)"
>
<slot name="left" :record="item"></slot>
</view>
<slot v-else name="left-list" :record="waterfall.left"> </slot>
</view>
<view
v-if="line === 2"
class="hy-virtual-container__list--right hy-virtual-container__list--box"
>
<view
v-if="slots.right"
class="hy-virtual-container__list--box-item"
v-for="item in waterfall.right"
:key="item[keyField]"
:style="itemStyle"
@click="handleClick(item)"
>
<slot name="right" :record="item"></slot>
</view>
<slot v-else name="right-list" :record="waterfall.right"> </slot>
</view>
</template>
<!--加载更多样式-->
</view>
<!-- <HyDivider :text="load" v-if="showDivider"></HyDivider>-->
</scroll-view>
</template>
<script lang="ts">
export default {
options: {
virtualHost: true,
},
};
</script>
<script lang="ts" setup>
import {
computed,
type CSSProperties,
nextTick,
onMounted,
reactive,
ref,
toRefs,
useSlots,
watch,
} from "vue";
import { addUnit, getPx, getRect } from "../../utils";
import type IProps from "./typing";
import defaultProps from "./props";
const props = withDefaults(defineProps<IProps>(), defaultProps);
const {
list,
line,
keyField,
itemHeight,
containerHeight,
marginBottom,
padding,
borderRadius,
background,
border,
} = toRefs(props);
const emit = defineEmits(["scrollButton", "click"]);
const slots = useSlots();
// 滚动条距离顶部距离
const scrollTop = ref(0);
// 可视区域的高度
const viewHeight = ref(0);
const waterfall: { left: AnyObject[]; right: AnyObject[] } = reactive({
left: [],
right: [],
});
// 排列方式
const arrange = computed(() => (line.value === 1 ? "column" : "row"));
const boxHeight = getPx(itemHeight.value) + getPx(marginBottom.value);
const listHeight = addUnit(containerHeight.value);
onMounted(() => {
nextTick(async () => {
const res = await getRect(".hy-virtual-container");
viewHeight.value = (res as UniApp.NodeInfo).height ?? 0;
});
});
const itemStyle = computed((): CSSProperties => {
return {
height: addUnit(itemHeight.value),
padding: addUnit(padding.value),
marginBottom: addUnit(marginBottom.value),
borderRadius: addUnit(borderRadius.value),
background: background.value,
border: border.value ? "1px solid #dadbde" : "",
};
});
/**
* @description 虚拟列表真实展示数据:起始下标
*/
const start = computed(() => {
const s = Math.floor(scrollTop.value / boxHeight);
return Math.max(0, s * line.value);
});
/**
* @description 虚拟列表真实展示数据:结束下标
*/
const over = computed(() => {
const o = Math.floor(
(scrollTop.value + viewHeight.value + 1) / boxHeight + 5,
);
return Math.min(list.value.length, o * line.value);
});
/**
* @description 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动)
*/
const paddingAttr = computed(() => {
const paddingTop = start.value * boxHeight;
const paddingBottom = (list.value.length - over.value) * boxHeight;
return `${paddingTop / line.value}px 0 ${paddingBottom / line.value}px`;
});
/**
* @description 虚拟列表真实展示数据
*/
const virtualData = computed(() => {
return list.value.slice(start.value, over.value);
});
watch(
() => virtualData.value,
(newVal, oldValue) => {
waterfall.left.length = 0;
waterfall.right.length = 0;
if (line.value === 2 && newVal.every((item) => typeof item !== "string")) {
newVal.forEach((item, i) => {
if (i % 2 === 0) {
waterfall.left.push(item);
} else {
waterfall.right.push(item);
}
});
}
},
{ immediate: true, deep: true },
);
/**
* @description 监听滚动条距离顶部距离,实时更新
*/
const onScroll = async (e: any) => {
scrollTop.value = e.detail.scrollTop ?? 0;
};
/**
* @description 滚动底部函数
* */
const scrollToLower = () => {
emit("scrollButton");
};
/**
* @description 点击行触发函数
* */
const handleClick = (temp: string | AnyObject) => {
emit("click", temp);
};
/**
* @description 获取默认插槽
*/
const slotDefault = useSlots().default;
</script>
<style lang="scss" scoped>
@use "../../theme.scss" as *;
@use "../../libs/css/mixin.scss" as *;
.hy-virtual-container {
height: v-bind(listHeight);
padding: 0 $hy-border-margin-padding-base;
box-sizing: border-box;
&__list {
padding: v-bind(paddingAttr);
@include flex(v-bind(arrange));
overflow-anchor: none;
&--item {
box-sizing: border-box;
}
&--left {
margin-right: $hy-border-margin-padding-base;
}
&--box {
box-sizing: border-box;
width: 50%;
display: flex;
flex-direction: column;
&-item {
box-sizing: border-box;
position: relative;
overflow: hidden;
}
}
}
}
</style>
如果你对实现细节仍有疑问,可以直接访问华玥组件库的虚拟列表组件页面,查看其源码以获取更多信息: 华玥组件库 虚拟列表List