Skip to content

Commit f2cf316

Browse files
author
Tenny
committed
Squashed commit of the following:
commit df5e027 Author: Tenny <tenny.shu@foxmail.com> Date: Wed Sep 11 12:06:21 2024 +0800 feat(VirtualList): 虚拟列表 commit fc0da47 Author: Tenny <joel.shu@qq.com> Date: Mon Sep 9 23:02:46 2024 +0800 feat(VirtualList): 完善功能 Signed-off-by: Tenny <joel.shu@qq.com> commit fa0f3a8 Author: Tenny <tenny.shu@foxmail.com> Date: Mon Sep 9 17:56:53 2024 +0800 feat(VirtualList): 初始渲染 commit 6f82190 Author: Tenny <joel.shu@qq.com> Date: Mon Sep 2 23:57:51 2024 +0800 feat(Virtual): 虚拟列表模板 Signed-off-by: Tenny <joel.shu@qq.com> commit 31c3f8a Author: Tenny <tenny.shu@foxmail.com> Date: Thu Aug 22 16:47:46 2024 +0800 chore: 自动脚本命令变更 publish -> pull 避免与 pnpm 冲突 Signed-off-by: Tenny <tenny.shu@foxmail.com>
1 parent 2f46bee commit f2cf316

File tree

11 files changed

+194
-3
lines changed

11 files changed

+194
-3
lines changed

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ export default defineConfig({
164164
text: 'List 滚动列表',
165165
link: '/components/list',
166166
},
167+
{
168+
text: 'VirtualList 虚拟列表',
169+
link: '/components/virtuallist',
170+
},
167171
{
168172
text: 'Tabs 标签页',
169173
link: '/components/tabs',

docs/.vitepress/theme/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import '../../../style/tag';
4040
import '../../../style/pagination';
4141
import '../../../style/tanstack-table';
4242
import '../../../style/md-input';
43+
import '../../../style/virtual-list';
4344

4445
export default {
4546
extends: DefaultTheme,

docs/components/virtuallist.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# VirtualList 虚拟列表
2+
3+
当我们遇到数据量很大的情况时,传统的渲染方式会导致浏览器负载过大,页面渲染速度慢,滚动卡顿等一系列问题。
4+
5+
对于这种列表数据,我们可以采用虚拟滚动来进行性能优化。
6+
7+
> 1. **现在暂时只支持固定高度的列表**
8+
> 2. 以后可以考虑使用 `css3` 属性 [content-visibility](https://developer.mozilla.org/zh-CN/docs/Web/CSS/content-visibility)
9+
10+
## 演示
11+
12+
<script setup>
13+
import { VirtualList } from "../../src"
14+
15+
const items = Array.from({ length: 10000 }, (_, i) => ({
16+
id: `${i}`,
17+
value: i,
18+
}))
19+
</script>
20+
21+
### 基础用法
22+
23+
对于固定高度, 一次性渲染 `1w` 条数据。`item-size` 表示每一行的高度
24+
25+
<ClientOnly>
26+
<CodePreview>
27+
<textarea lang="vue" v-pre>
28+
<script setup lang="ts">
29+
const items = Array.from({ length: 10000 }, (_, i) => ({
30+
id: `${i}`,
31+
value: i,
32+
}));
33+
</script>
34+
<template>
35+
<nt-virtual-list :items="items" :item-size="42" key-field="id">
36+
<template #default="{ item }">
37+
<span>{{ item.value }}</span>
38+
</template>
39+
</nt-virtual-list>
40+
</template>
41+
</textarea>
42+
<template #preview>
43+
<div class="virtual-list-demo-container">
44+
<VirtualList :items="items" :item-size="42" key-field="id">
45+
<template #default="{ item }">
46+
<span>{{ item.value }}</span>
47+
</template>
48+
</VirtualList>
49+
</div>
50+
</template>
51+
</CodePreview>
52+
</ClientOnly>
53+
54+
## API
55+
56+
### VirtualList Props
57+
58+
<!-- prettier-ignore -->
59+
| 参数 | 说明 | 类型 | 默认值 |
60+
| --- | --- | --- | --- |
61+
| `items` | *必传*, 需要展示的数据 | `any[]` | - |
62+
| `item-size` | *必传*, 表项的高度,用于计算滚动大小和位置 | `number` | - |
63+
| `item-class` | 列表项的 `class` | `string` | - |
64+
| `key-field` | 选项 `key` 的字段名, 用于 `v-for``key`, 不传则用 `index` | `string` | - |
65+
| `buffer` | 冲区数量,列表会在上下多渲染额外的项 | `number` | `2` |
66+
67+
### VirtualList Slots
68+
69+
<!-- prettier-ignore -->
70+
| 名称 | 说明 | 字段 |
71+
| --- | --- | --- |
72+
| `default` | 渲染列表项内容 | `item`: 列表项数据 |
73+
| `render` | 渲染整个列表项, 需要手动遍历数据列表进行渲染 | `items`: 可现实列表数据 |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"docs:preview": "vitepress preview docs",
1212
"inspect:eslint": "eslint --inspect-config",
1313
"push": "pwsh scripts/publish.ps1 push",
14-
"publish": "./scripts/publish.ps1 publish"
14+
"pull": "pwsh scripts/publish.ps1 publish"
1515
},
1616
"devDependencies": {
1717
"@types/node": "^22.5.0",

scripts/publish.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ if ($Cmd -eq "push") {
2121
git merge dev -m "new version"
2222
git push origin main
2323

24-
Write-Host $Message
24+
Write-Host " "
25+
Write-Host "提交成功" -ForegroundColor Green
2526
} elseif (($Cmd -eq "pull") -or ($Cmd -eq "publish")) {
2627
git pull origin main
2728
pnpm lib:build

src/app_components/SourceCode.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ onMounted(async () => {
7979
const parser = new DOMParser();
8080
const doc = parser.parseFromString(preCode, 'text/html');
8181
const children = doc.body.children;
82-
8382
for (let i = 0, len = children.length; i < len; i++) {
8483
if (children[i] != null) {
8584
fragment.appendChild(children[i]);

src/components/VirtualList.vue

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<template>
2+
<div class="nt-virtual-list" ref="$list" @scroll.passive="handleScroll">
3+
<div :style="{ height: `${totalSize}px` }">
4+
<div :style="{ transform: `translate3d(0px, ${startOffset}px, 0px)` }">
5+
<slot name="render" :data="visibleData">
6+
<!-- 列表项 -->
7+
<div
8+
v-for="(item, index) in visibleData"
9+
:style="{ height: props.itemSize + 'px' }"
10+
:key="keyField != null ? (item as any)[keyField] : index"
11+
:class="['nt-virtual-item', itemClass]"
12+
>
13+
<slot :item="item"></slot>
14+
</div>
15+
</slot>
16+
</div>
17+
</div>
18+
</div>
19+
</template>
20+
<script setup lang="ts" generic="T">
21+
import { onMounted, ref } from 'vue';
22+
23+
const props = withDefaults(
24+
defineProps<{
25+
/** 需要展示的数据 */
26+
items: T[];
27+
/** 列表项的高度,用于计算滚动大小和位置 */
28+
itemSize: number;
29+
itemClass?: string;
30+
/** 选项 key 的字段名, 用于 v-for 的 key */
31+
keyField?: string;
32+
/** 缓冲区数量,列表会在上下多渲染额外的项 */
33+
buffer?: number;
34+
}>(),
35+
{
36+
itemClass: '',
37+
buffer: 2,
38+
},
39+
);
40+
41+
const $list = ref<HTMLDivElement>();
42+
43+
/** 实际显示的数据列表 */
44+
const visibleData = ref<T[]>([]);
45+
/** 容器总高度 */
46+
const totalSize = ref(0);
47+
/** 滑动便宜 */
48+
const startOffset = ref(0);
49+
50+
/** 可视区域能够展示的最大元素个数 */
51+
let numVisible = 0;
52+
53+
/** 滑动延迟处理,滑动完成后,延迟处理更新数据,避免频繁触发数据更新 */
54+
let _t = -1;
55+
56+
function updateVisibleItems() {
57+
if ($list.value != null) {
58+
// 当前滚动位置
59+
const scrollTop = $list.value.scrollTop;
60+
/** 可视区域开始索引 */
61+
let startIndex = Math.floor(scrollTop / props.itemSize);
62+
/** 上缓冲区起始索引 */
63+
let topStartIndex = Math.max(0, startIndex - props.buffer);
64+
/** 下缓冲区结束索引 */
65+
const endIndex = Math.min(
66+
props.items.length,
67+
startIndex + numVisible + props.buffer,
68+
);
69+
// 偏移量, 当滑动位置是某一项的一部分的时候,计算已经滚动的那一部分距离
70+
let offset = scrollTop - (scrollTop % props.itemSize);
71+
// 生成可视区域数据
72+
visibleData.value = props.items.slice(topStartIndex, endIndex) as any[];
73+
/** 当前显示index-缓冲区的index就是缓冲区数量 */
74+
startOffset.value = offset - (startIndex - topStartIndex) * props.itemSize;
75+
}
76+
}
77+
78+
function handleScroll() {
79+
cancelAnimationFrame(_t);
80+
_t = requestAnimationFrame(() => {
81+
updateVisibleItems(); // 重新渲染数据
82+
});
83+
}
84+
85+
onMounted(() => {
86+
totalSize.value = props.itemSize * props.items.length;
87+
if ($list.value != null) {
88+
let rect = $list.value.getBoundingClientRect();
89+
numVisible = Math.ceil(rect.height / props.itemSize);
90+
updateVisibleItems(); // 初始化渲染数据
91+
}
92+
});
93+
</script>

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ export { default as Loading } from './directives/loading';
7171
export { default as PageHeader } from './components/PageHeader.vue';
7272
export { default as Tag } from './components/Tag.vue';
7373
export { default as Pagination } from './components/Pagination.vue';
74+
export { default as VirtualList } from "./components/VirtualList.vue";

src/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,7 @@
237237
select {
238238
appearance: auto;
239239
}
240+
241+
.virtual-list-demo-container {
242+
height: 240px;
243+
}

style/virtual-list/index.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.nt-virtual-list {
2+
height: 100%;
3+
overflow-y: auto;
4+
-webkit-overflow-scrolling: touch;
5+
position: relative;
6+
}
7+
.nt-virtual-content {
8+
position: absolute;
9+
top: 0;
10+
width: 100%;
11+
}
12+
.nt-virtual-item {
13+
overflow: hidden;
14+
}

0 commit comments

Comments
 (0)