前端在渲染大型列表时,会变得很慢,因为浏览器要处理大量的DOM节点。在真实场景下,我们不需要立刻渲染出全部的列表,只需要渲染一小部分,当用户鼠标滚动时,再去加载数据,即通过列表虚拟化来提升性能。
一、列表虚拟化
在实际的使用场景中,大型列表每个数据项的可能是复杂数据项,可能会包含图片、视频等等,当前端去渲染时,需要分类处理一下。
当数据项中包含图片时,做一个简单的demo实现:
基本思路
1.列表项数据结构:
每个列表项包含一个图片 URL。
2.懒加载图片:
只有当列表项进入视口时,才加载相应的图片。
3.结合虚拟化列表:
使用虚拟化列表技术,只渲染当前可视区域内的列表项,减少 DOM 操作和资源占用。
4.滚动监听:
监听滚动事件,动态计算哪些列表项进入了可视区域,以及何时加载它们的图片。
<template>
<div class="virtual-list-container" ref="container" @scroll="handleScroll">
<div :style="{ height: totalHeight + 'px' }">
<div v-for="(item, index) in visibleItems" :key="item.id"
:style="{ transform: 'translateY(' + itemPositions[index] + 'px)' }"
class="virtual-list-item">
<img v-if="item.isVisible" :src="item.imageUrl" @load="handleImageLoad(index)"
class="lazy-load-image" />
<div v-else class="placeholder" style="height: 200px; background-color: #f0f0f0;"></div>
<p>{{ item.text }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [], // 数据源
visibleItems: [], // 当前可见的列表项
itemPositions: [], // 可见列表项的位置
itemHeight: 200, // 每个列表项的高度
totalHeight: 0, // 虚拟列表容器的总高度
startIndex: 0, // 可视区域内的起始索引
endIndex: 0, // 可视区域内的结束索引
bufferSize: 5, // 缓冲区大小,可根据实际情况调整
};
},
mounted() {
this.initializeList();
},
methods: {
initializeList() {
// 模拟生成大量数据
this.items = Array.from({ length: 1000 }, (_, index) => ({
id: index,
text: `Item ${index + 1}`,
imageUrl: `via.placeholder.com/300?text=Image${index + 1}`, // 示例图片 URL
isVisible: false, // 标记是否可见
}));
// 计算虚拟列表容器的总高度
this.totalHeight = this.items.length * this.itemHeight;
// 初始化可见列表项和位置
this.updateVisibleItems();
},
updateVisibleItems() {
const container = this.$refs.container;
const scrollTop = container.scrollTop;
const offset = Math.floor(scrollTop / this.itemHeight) - this.bufferSize;
this.startIndex = Math.max(offset, 0);
this.endIndex = Math.min(this.startIndex + this.visibleCount + 2 * this.bufferSize, this.items.length);
this.visibleItems = this.items.slice(this.startIndex, this.endIndex);
this.itemPositions = this.visibleItems.map((item, index) => (this.startIndex + index) * this.itemHeight);
// 标记可见项为可见,用于懒加载图片
this.visibleItems.forEach(item => {
if (!item.isVisible) {
item.isVisible = true;
}
});
},
handleScroll() {
this.updateVisibleItems();
},
handleImageLoad(index) {
// 图片加载完成时的处理逻辑,可根据需求自定义
console.log(`Image ${index + 1} loaded.`);
}
},
computed: {
visibleCount() {
return Math.ceil(this.$refs.container.clientHeight / this.itemHeight);
}
}
};
</script>
<style>
.virtual-list-container {
height: 400px;
overflow-y: scroll;
border: 1px solid #ccc;
}
.virtual-list-item {
height: 200px; /* 每个列表项的固定高度 */
border-bottom: 1px solid #eee;
padding: 10px;
box-sizing: border-box;
}
.lazy-load-image {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.placeholder {
width: 100%;
}
</style>
解释和注意事项
1.数据源和初始化:
items 数组中的每个对象包含了一个图片的 URL (imageUrl) 和一个标志 (isVisible),用来表示图片是否已经在可见区域内加载。
2.图片加载机制:
在 v-for 中,根据 isVisible 的值决定是渲染 <img> 元素还是一个占位符 <div>。只有当列表项进入可视区域后,才加载相应的图片。
3.滚动事件处理:
handleScroll 方法监听滚动事件,根据滚动位置动态更新可见区域内的列表项。
4.懒加载图片:
当图片加载时,可以在 handleImageLoad 方法中执行额外的逻辑,例如记录日志或更新 UI 状态。
5.样式:
使用 CSS 控制列表项和图片的样式,确保列表的外观和交互符合设计需求。
通过这种方式,可以有效地结合虚拟化和图片懒加载技术,优化大型数据列表的性能,并提升用户体验。
vue官方给出三个虚拟化的社区库供使用:
● vue-virtual-scroller
● vue-virtual-scroll-grid
● vueuc/VVirtualList
二、浅层式API
除了列表虚拟化可以较好的实现大型列表优化,也可以结合浅层式API,对数据做浅度监听
Vue 的响应性系统默认是深度的。当在数据量巨大时,深度响应性也性能负担较大,因为每个属性访问都将触发代理的依赖追踪。
Vue 对上述问题提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新。
vue官方给出的例子如下:
const shallowArray = shallowRef([
/* 巨大的列表,里面包含深层的对象 */
])
// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]
// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]
通过上述方式,在一定程度上降低了浏览器对大型列表渲染的负担,并提升用户体验。