Vue 虚拟列表使用
简介
所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。
但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。
文件目录
|-- ./src
| |-- ./src/App.vue
| |-- ./src/components
| | |-- ./src/components/MyTest.vue
| | |-- ./src/components/Person.vue
| |-- ./src/main.js
| `-- ./src/randomData.js
npm i vue-virtual-scroller
vue-virtual-scroller API:https://github.com/Akryum/vue-virtual-scroller/blob/master/packages/vue-virtual-scroller/README.md
// ./src/main.js
import { createApp } from "vue";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import VueVirtualScroller from "vue-virtual-scroller";
import App from "./App.vue";
const app = createApp(App);
app.use(VueVirtualScroller);
app.mount("#app");
<!-- ./src/App.vue -->
<script setup>
import MyTest from "./components/MyTest.vue";
</script>
<template>
<MyTest />
</template>
<style scoped>
html,
body,
#app {
box-sizing: border-box;
height: 100%;
}
body {
font-size: 16px;
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: 0;
}
#app,
.page {
display: flex;
flex-direction: column;
align-items: stretch;
}
.menu,
.page {
padding: 12px;
box-sizing: border-box;
}
.package {
margin-right: 12px;
}
.package-name {
font-family: monospace;
color: #2c3e50;
background: #eee;
padding: 5px 12px 3px;
}
.vue-recycle-scroller {
-webkit-overflow-scrolling: touch;
}
.vue-recycle-scroller__item-container,
.vue-recycle-scroller__item-wrapper {
box-sizing: border-box;
}
.vue-recycle-scroller__item-view {
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
.tr,
.td {
box-sizing: border-box;
}
.vue-recycle-scroller__item-view .tr {
display: flex;
align-items: center;
}
.vue-recycle-scroller__item-view .td {
display: block;
}
.toolbar {
flex: auto 0 0;
text-align: center;
margin-bottom: 12px;
line-height: 32px;
position: sticky;
top: 0;
z-index: 9999;
background: white;
}
.recycle-scroller-demo.page-mode .toolbar {
border-bottom: solid 1px #e0edfa;
}
.toolbar > *:not(:last-child) {
margin-right: 24px;
}
.avatar {
background: grey;
}
</style>
// ./src/randomData.js
import { faker } from "@faker-js/faker/locale/zh_CN";
// 处理时间
function handleDate(date) {
if (!date.getFullYear && !date.getMonth && !date.getDate) return;
function checkTime(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
return (
checkTime(date.getFullYear()) +
"-" +
checkTime(date.getMonth() + 1) +
"-" +
checkTime(date.getDate())
);
}
// 生成数据
function generateData(count) {
if (!count || Number(count) === 0) {
count = 10;
}
const namesArr = [];
for (let i = 0; i < count; i++) {
const birthday = faker.date.birthdate({ min: 18, max: 65, mode: "age" });
namesArr.push({
id: faker.datatype.uuid(),
index: i + 1,
height: 20,
value: { name: faker.name.fullName(), birthday: handleDate(birthday) },
});
}
return namesArr;
}
export { generateData };
<script setup>
// ./src/components/MyTest.vue
import { watch, computed, onMounted, nextTick, ref } from "vue";
import Person from "./Person.vue";
import { generateData } from "../randomData";
const items = ref([]);
const count = ref(10000);
const renderScroller = ref(true);
const showScroller = ref(true);
const buffer = ref(200);
const pageMode = ref(true);
const pageModeFullPage = ref(true);
const scrollTo = ref(200);
const scroller = ref(null);
const list = computed(() => {
return items.value.map((item) => {
return Object.assign({}, item);
});
});
const generateItems = () => {
console.log("Generating " + count.value + " items...");
const time = Date.now();
const _items = generateData(count.value);
console.log(
"Generated " + _items.length + " in " + (Date.now() - time) + "ms"
);
items.value = _items;
};
watch(count, () => generateItems());
onMounted(() => {
nextTick(() => generateItems());
window.scroller = scroller.value;
});
</script>
<template>
<div
class="recycle-scroller-demo"
:class="{
'page-mode': pageMode,
'full-page': pageModeFullPage,
}"
>
<div class="toolbar">
<span>
<button @mousedown="scroller.scrollToItem(scrollTo)">Scroll To:</button>
<input
v-model.number="scrollTo"
type="number"
min="0"
:max="list.length - 1"
/>
</span>
<span>
<button @mousedown="renderScroller = !renderScroller">
Toggle render
</button>
<button @mousedown="showScroller = !showScroller">
Toggle visibility
</button>
</span>
</div>
<div
v-if="renderScroller"
v-show="showScroller"
class="content"
>
<div class="wrapper">
<RecycleScroller
:key="pageModeFullPage"
ref="scroller"
class="scroller"
:items="list"
:buffer="buffer"
:page-mode="pageMode"
key-field="id"
size-field="height"
>
<template #before>
<h3>表头占位</h3>
</template>
<template #default="props">
<Person
:item="props.item"
:index="props.index"
/>
</template>
</RecycleScroller>
</div>
</div>
<div v-else>
<h2>My Components</h2>
</div>
</div>
</template>
<style scoped>
.recycle-scroller-demo:not(.page-mode) {
height: 100%;
display: flex;
flex-direction: column;
}
.recycle-scroller-demo.page-mode:not(.full-page) {
height: 100%;
}
.recycle-scroller-demo.page-mode {
flex: auto 0 0;
}
.recycle-scroller-demo.page-mode .toolbar {
border-bottom: solid 1px #e0edfa;
}
.content {
flex: 100% 1 1;
border: solid 1px #42b983;
position: relative;
}
.recycle-scroller-demo.page-mode:not(.full-page) .content {
overflow: auto;
}
.recycle-scroller-demo:not(.page-mode) .wrapper {
overflow: hidden;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.scroller {
width: 100%;
height: 100%;
}
</style>
<!-- ./src/components/Person.vue -->
<template>
<div
class="tr person"
@click="edit"
>
<div class="td index">{{ index }}</div>
<div class="td">
<div class="info">
<span class="name">{{ item.value.name }}</span>
<span class="birthday">{{ item.value.birthday }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["item", "index"],
methods: {
edit() {
this.item.value.birthday += "#";
},
},
};
</script>
<style scoped>
.hover > .tr.person {
background-color: lightblue;
}
.tr.person {
display: flex;
}
.index {
color: lightcoral;
width: 55px;
text-align: right;
flex: auto 0 0;
margin-right: 20px;
}
.person .info {
display: flex;
align-items: center;
}
.name {
margin-right: 20px;
}
</style>