您的位置:首页 > 教育 > 锐评 > 一文拿下长列表渲染(分页、懒加载、虚拟列表)

一文拿下长列表渲染(分页、懒加载、虚拟列表)

2024/10/6 10:30:24 来源:https://blog.csdn.net/XiugongHao/article/details/141931235  浏览:    关键词:一文拿下长列表渲染(分页、懒加载、虚拟列表)

讲解

分页

  • 优点:快速跳转,方便回溯(更多用在 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><buttonv-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><!-- 渲染可见的列表项 --><divv-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>

在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com