讲解
分页
- 优点:快速跳转,方便回溯(更多用在 B 端)
- 缺点:用户体验不好,需要手动切换页码
懒加载
- 优点:用户体验好,不像分页那样需要手动切换(更多用在 C 端)
- 缺点:不方便跳转回溯,而且
scroll事件可能会被高频度的触发,并且在scroll事件里又去操作dom,页面便会重排和重绘,造成卡顿
虚拟列表
- 优点:dom 节点数不变,动态替换,解决了懒加载造成回流重绘的性能问题(更多用在 C 端)
实现
最终测试,除了普通长列表渲染,其他渲染都使得 10w 条数据渲染时间由 4s 变为 0.4 s。
以下长列表渲染均通过代码示例进行讲解。
代码如下:
App.vue
<template>
<button @click="toPage('/demo')">demo</button>
<button @click="toPage('/page-demo')">page-demo</button>
<button @click="toPage('/virtual-demo')">virtual-demo</button>
<button @click="toPage('/lazy-demo')">lazy-demo</button>
<button @click="toPage('/virtual-plus-demo')">virtual-plus-demo</button>
<div>
<router-view></router-view>
</div>
</template>
<script setup lang="ts">
import {useRouter} from "vue-router";
const router = useRouter()
const toPage = (path) => {
router.push(path)
}
</script>
<style scoped>
</style>
main.js
虚拟列表库使用(Vue3版本)vue-virtual-scroller/packages/vue-virtual-scroller at master · Akryum/vue-virtual-scroller (github.com)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import VueVirtualScroller from 'vue-virtual-scroller'
const app = createApp(App)
app.use(router)
app.use(VueVirtualScroller)
app.mount('#app')
router/index.js
import {createRouter, createWebHistory} from 'vue-router'
import HomeView from '../views/PageDemoView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/demo',
name: 'demo',
component: () => import('../views/DemoView.vue')
},
{
path: '/page-demo',
name: 'pageDemo',
component: () => import('../views/PageDemoView.vue')
},
{
path: '/lazy-demo',
name: 'lazyDemo',
component: () => import('../views/LazyDemoView.vue')
},
{
path: '/virtual-demo',
name: 'virtualDemo',
component: () => import('../views/VirtualDemoView.vue')
},
{
path: '/virtual-plus-demo',
name: 'virtualPlusDemo',
component: () => import('../views/VirtualPlusDemoView.vue')
},
]
})
export default router
长列表渲染
DemoView.vue
<template>
<div>
<h1>长列表 Demo</h1>
<ul>
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 使用 ref 定义响应式数据
const items = ref([]);
// 定义生成数据的方法
const generateItems = (count) => {
for (let i = 0; i < count; i++) {
items.value.push({ id: i, name: `Item ${i}` });
}
};
// 在组件挂载时生成数据 假定十万条数据
onMounted(() => {
generateItems(100000);
});
</script>
<style scoped>
.list-item {
padding: 10px;
border-bottom: 1px solid #ddd;
}
</style>
长列表渲染 - 懒加载
LazyDemeView.vue
<template>
<div class="scroll-container" @scroll="handleScroll">
<h1>懒加载的长列表 Demo</h1>
<ul>
<li v-for="item in currentItems" :key="item.id" class="list-item">
{{ item.name }}
</li>
</ul>
<p v-if="loading" class="loading">加载中...</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 全部列表项
const items = ref([]);
// 当前显示的列表项
const currentItems = ref([]);
// 每次加载的数量
const batchSize = 20;
// 当前加载到的索引
const currentIndex = ref(0);
// 是否正在加载数据
const loading = ref(false);
// 模拟生成 10000 条数据
const generateItems = (count) => {
for (let i = 0; i < count; i++) {
items.value.push({ id: i, name: `Item ${i}` });
}
};
// 加载数据
const loadMoreItems = () => {
if (loading.value) return;
loading.value = true;
// 模拟加载数据的延迟
setTimeout(() => {
const nextItems = items.value.slice(currentIndex.value, currentIndex.value + batchSize);
currentItems.value.push(...nextItems);
currentIndex.value += batchSize;
loading.value = false;
}, 1000); // 模拟1秒加载时间
};
// 处理滚动事件
const handleScroll = (e) => {
const container = e.target;
const bottomReached = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
if (bottomReached && !loading.value) {
loadMoreItems();
}
};
// 初次加载时生成数据
onMounted(() => {
generateItems(100000);
loadMoreItems(); // 初次加载数据
});
</script>
<style scoped>
.scroll-container {
height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.loading {
text-align: center;
padding: 10px;
color: #007bff;
}
</style>
长列表渲染 - 分页
PageDemoView.vue
<template>
<div>
<h1>分页器的长列表渲染 Demo</h1>
<!-- 显示当前页的数据 -->
<ul class="list">
<li v-for="item in paginatedData" :key="item.id" class="list-item">
{{ item.name }}
</li>
</ul>
<!-- 分页器 -->
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<button
v-for="page in paginationPages"
:key="page"
@click="goToPage(page)"
:disabled="page === '...' || currentPage === page"
:class="{ active: currentPage === page }"
>
{{ page }}
</button>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 每页项目数量
const itemsPerPage = 20;
// 总项目数量(模拟)
const totalItems = 1000;
// 当前页码
const currentPage = ref(1);
// 模拟生成数据
const items = ref([]);
for (let i = 0; i < totalItems; i++) {
items.value.push({ id: i, name: `Item ${i + 1}` });
}
// 计算总页数
const totalPages = computed(() => Math.ceil(totalItems / itemsPerPage));
// 计算当前页的显示数据
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
return items.value.slice(start, end);
});
// 动态生成分页页码的逻辑
const paginationPages = computed(() => {
const pages = [];
const total = totalPages.value;
const current = currentPage.value;
if (total <= 7) {
// 如果总页数小于等于 7,直接显示所有页码
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
// 总页数大于 7 的情况
const startPage = Math.max(2, current - 1);
const endPage = Math.min(total - 1, current + 1);
// 添加第一页
pages.push(1);
// 添加省略号或开始页码
if (startPage > 2) {
pages.push('...');
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
// 添加省略号或结束页码
if (endPage < total - 1) {
pages.push('...');
}
// 添加最后一页
pages.push(total);
}
return pages;
});
// 跳转到下一页
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// 跳转到上一页
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
// 跳转到指定页
const goToPage = (page) => {
if (page !== '...') {
currentPage.value = page;
}
};
</script>
<style scoped>
.list {
padding: 0;
margin: 0;
list-style: none;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.pagination button {
padding: 10px 15px;
margin: 0 5px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
}
.pagination button.active {
background-color: #0056b3;
}
.pagination button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>
长列表渲染 - 虚拟列表(手写)
VirtualDemoView.vue
<template>
<h1>虚拟列表 Demo</h1>
<div class="scroll-container" @scroll="onScroll">
<!-- 虚拟列表中的填充高度确保滚动条 -->
<div :style="{ height: totalHeight + 'px' }"></div>
<!-- 渲染可见的列表项 -->
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
:style="{ top: item.top + 'px' }"
>
{{ item.name }}
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
// 每个列表项的高度
const itemHeight = 50;
// 总项目数量
const totalItems = 100000;
// 可视区域的高度
const visibleHeight = 500;
// 计算可视区域中可见的项目数量
const visibleCount = Math.ceil(visibleHeight / itemHeight) + 1; // 多加1项以避免边界问题
// 保存所有的项目
const items = ref([]);
// 当前可见区域内的起始索引
const startIndex = ref(0);
// 计算整个列表的总高度,用于创建滚动条
const totalHeight = computed(() => totalItems * itemHeight);
// 可见的项目列表
const visibleItems = ref([]);
// 生成虚拟项目数据
const generateItems = (count) => {
for (let i = 0; i < count; i++) {
items.value.push({ id: i, name: `Item ${i}` });
}
};
// 更新可见项目的逻辑
const updateVisibleItems = () => {
const visible = [];
for (let i = startIndex.value; i < Math.min(startIndex.value + visibleCount, totalItems); i++) {
visible.push({
id: items.value[i].id,
name: items.value[i].name,
top: i * itemHeight,
});
}
visibleItems.value = visible;
};
// 处理滚动事件
const onScroll = (e) => {
const scrollTop = e.target.scrollTop;
startIndex.value = Math.floor(scrollTop / itemHeight);
updateVisibleItems();
};
// 初始化逻辑
onMounted(() => {
generateItems(totalItems); // 生成项目数据
updateVisibleItems(); // 初始化可见的项目
});
</script>
<style scoped>
.scroll-container {
height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
position: relative;
}
.list-item {
position: absolute;
width: 100%;
height: 50px;
display: flex;
align-items: center;
padding: 0 10px;
box-sizing: border-box;
border-bottom: 1px solid #ddd;
}
</style>
长列表渲染 - 虚拟列表(库)
VietualPlusDemoView.vue
<template>
<div style="height: 100vh;">
<h1>虚拟列表 Plus Demo</h1>
<RecycleScroller
:items="items"
:item-size="50"
class="virtual-list"
v-slot="{ item }"
>
<div class="list-item">
{{ item.name }}
</div>
</RecycleScroller>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 模拟生成大量数据
const items = ref([]);
const generateItems = (count) => {
for (let i = 0; i < count; i++) {
items.value.push({ id: i, name: `Item ${i}` });
}
};
generateItems(100000);
</script>
<style scoped>
.virtual-list {
height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
}
.list-item {
height: 50px;
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #ddd;
}
</style>