Vue中實現(xiàn)虛擬列表的4種技術(shù)方案調(diào)研
背景
什么是虛擬列表
虛擬列表(Virtual List)是一種優(yōu)化長列表渲染性能的技術(shù)。其核心思想是只渲染可見區(qū)域的列表項,而不是渲染所有數(shù)據(jù)。當用戶滾動時,動態(tài)地創(chuàng)建和銷毀 DOM 元素,從而大幅減少 DOM 節(jié)點數(shù)量,提升渲染性能。
本篇文章中,筆者從Vue3的技術(shù)框架來講解虛擬列表不同場景中的用法,不涉及虛擬列表的原理。社區(qū)里有很多講解虛擬列表的原理的文章,筆者不再贅述。筆者從虛擬列表的分類上統(tǒng)籌每一種虛擬列表技術(shù)方案,并給出參考示例。
為什么使用虛擬列表
傳統(tǒng)列表的性能瓶頸:
// 渲染 10000 條數(shù)據(jù)
const items = Array.from({ length: 10000 }, (_, i) => i)
// ? 傳統(tǒng)方式:創(chuàng)建 10000 個 DOM 節(jié)點
<div v-for="item in items" :key="item">{{ item }}</div>
// 結(jié)果:首次渲染緩慢、滾動卡頓、內(nèi)存占用高
虛擬列表的優(yōu)勢:
- 虛擬列表:只渲染可見的 ~10-20 個節(jié)點
- 即使有 10000 條數(shù)據(jù),DOM 中只有可見區(qū)域的節(jié)點
- 結(jié)果:快速渲染、流暢滾動、低內(nèi)存占用
@vueuse/core 簡介
@vueuse/core 是一個強大的 Vue 3 組合式 API 工具集,其中 useVirtualList 提供了開箱即用的虛擬列表解決方案。后續(xù)所有的示例代碼均使用 useVirtualList 實現(xiàn)
快速開始
npm install @vueuse/core # 或 pnpm add @vueuse/core
不得不說,Vue的技術(shù)生態(tài)中,很多第三方依賴開箱即用,這一點非常棒!
useVirtualList API 詳解
基本用法
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
items, // 數(shù)據(jù)源
options // 配置選項
)
參數(shù)說明
items (必需)
類型: MaybeRef<T[]>
說明: 要渲染的數(shù)據(jù)數(shù)組,可以是響應式引用或普通數(shù)組
// 方式 1: 響應式引用 const items = ref([1, 2, 3, ...]) // 方式 2: 普通數(shù)組 const items = [1, 2, 3, ...] // 方式 3: computed const items = computed(() => originalData.filter(...))
options (必需)
類型: UseVirtualListOptions
| 選項 | 類型 | 默認值 | 說明 |
|---|---|---|---|
| itemHeight | number | (index: number) => number | 必需 | 項目高度,可以是固定值或計算函數(shù) |
| overscan | number | 5 | 預渲染的額外項目數(shù)量,提升滾動流暢度 |
返回值說明
list
類型: ComputedRef<ListItem<T>[]>
說明: 當前應該渲染的列表項數(shù)組
interface ListItem<T> {
index: number // 在原數(shù)組中的索引
data: T // 原始數(shù)據(jù)
}
使用示例:
<div v-for="item in list" :key="item.index">
<div>索引: {{ item.index }}</div>
<div>數(shù)據(jù): {{ item.data }}</div>
</div>
containerProps (非常重要)
類型: Object
說明: 需要綁定到容器元素的屬性對象
包含屬性:
ref: 容器的引用onScroll: 滾動事件處理函數(shù)style: 容器樣式(可能包含 overflow 等)
使用方式:
<div v-bind="containerProps" style="height: 400px; overflow-y: auto;"> <!-- 內(nèi)容 --> </div>
wrapperProps (非常重要)
類型: Object
說明: 需要綁定到包裝器元素的屬性對象
包含屬性:
style: 包裝器樣式,包含計算的總高度
作用:
- 設(shè)置內(nèi)部容器的總高度(基于所有項目的總高度)
- 創(chuàng)建正確的滾動區(qū)域
- 支持虛擬化定位
使用方式:
<div v-bind="containerProps">
<div v-bind="wrapperProps">
<!-- 列表項 -->
</div>
</div>
工作原理

關(guān)鍵點:
- 容器(Container): 固定高度,overflow 可滾動
- 包裝器(Wrapper): 高度 = 所有項目的總高度
- 列表項: 只渲染可見區(qū)域的項目 (useVirtualList返回的 listitems)
四種虛擬列表實現(xiàn)方案
經(jīng)過筆者的調(diào)研,主要有四種虛擬列表技術(shù)方案
固定項高度 + 固定容器高度
方案特點
- 項目高度: 固定(如 50px)
- 容器高度: 固定(如 400px)
- 適用場景: 標準列表、表格、聊天記錄等統(tǒng)一高度的場景
適用場景
| 場景 | 說明 |
|---|---|
| 數(shù)據(jù)列表 | 表格數(shù)據(jù)、商品列表、用戶列表 |
| 日志查看器 | 系統(tǒng)日志、操作記錄 |
| 聊天記錄 | 簡單文本消息列表 |
| 文件瀏覽器 | 文件/文件夾列表 |
完整實現(xiàn)代碼
組件代碼: FixedHeightVirtualList.vue
<script setup>
import { ref } from "vue";
import { useVirtualList } from "@vueuse/core";
const props = defineProps({
items: {
type: Array,
default: () => [],
required: true,
},
itemHeight: {
type: Number,
default: 50,
},
containerHeight: {
type: Number,
default: 400,
},
});
const container = ref(null);
// ?? 關(guān)鍵配置:itemHeight 為固定數(shù)值
const { list, containerProps, wrapperProps } = useVirtualList(props.items, {
itemHeight: props.itemHeight, // 固定高度
overscan: 5,
});
</script>
<template>
<div
ref="container"
class="virtual-list-container"
:style="{ height: `${containerHeight}px` }" <!-- 固定容器高度 -->
v-bind="containerProps"
>
<div class="virtual-list-wrapper" v-bind="wrapperProps">
<div
v-for="item in list"
:key="item.index"
class="virtual-list-item"
:style="{ height: `${itemHeight}px` }" <!-- 固定項目高度 -->
>
<div class="item-content">
<span class="item-index">{{ item.data.id }}</span>
<span class="item-text">{{ item.data.name }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list-container {
overflow: hidden; /* ?? 關(guān)鍵:overflow hidden */
border: 1px solid #e8e8e8;
border-radius: 8px;
}
.virtual-list-item {
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
</style>
使用示例
<script setup>
import { ref } from 'vue'
import FixedHeightVirtualList from './components/FixedHeightVirtualList.vue'
const items = ref(
Array.from({ length: 1000 }, (_, index) => ({
id: index + 1,
name: `項目 ${index + 1}`
}))
)
</script>
<template>
<FixedHeightVirtualList
:items="items"
:item-height="50"
:container-height="400"
/>
</template>
優(yōu)點
- 實現(xiàn)簡單,性能最優(yōu)
- 滾動最流暢
- 計算開銷最小
缺點
- 所有項目必須高度一致
- 容器高度固定,不夠靈活
固定項高度 + 動態(tài)容器高度
方案特點
- 項目高度: 固定(如 50px)
- 容器高度: 動態(tài)自適應(max-height)
- 適用場景: 響應式布局、下拉菜單、彈窗列表
適用場景
| 場景 | 說明 |
|---|---|
| 下拉選擇器 | 數(shù)據(jù)量不確定的下拉列表 |
| 搜索建議 | 搜索結(jié)果列表(數(shù)量動態(tài)變化) |
| 彈窗列表 | Modal 中的列表內(nèi)容 |
| 側(cè)邊欄菜單 | 響應式側(cè)邊導航 |
| 響應式布局 | 需要適應不同屏幕尺寸 |
完整實現(xiàn)代碼
組件代碼: DynamicHeightVirtualList.vue
<script setup>
import { ref } from "vue";
import { useVirtualList } from "@vueuse/core";
const props = defineProps({
items: {
type: Array,
default: () => [],
required: true,
},
itemHeight: {
type: Number,
default: 50,
},
maxHeight: {
type: Number,
default: 500,
},
});
const container = ref(null);
// ?? 關(guān)鍵配置:itemHeight 為固定數(shù)值
const { list, containerProps, wrapperProps } = useVirtualList(props.items, {
itemHeight: props.itemHeight,
overscan: 5,
});
</script>
<template>
<div
ref="container"
class="virtual-list-container"
:style="{ maxHeight: `${maxHeight}px` }" <!-- ?? max-height 實現(xiàn)動態(tài) -->
v-bind="containerProps"
>
<div class="virtual-list-wrapper" v-bind="wrapperProps">
<div
v-for="item in list"
:key="item.index"
class="virtual-list-item"
:style="{ height: `${itemHeight}px` }"
>
<div class="item-content">
<span class="item-index">{{ item.data.id }}</span>
<span class="item-text">{{ item.data.name }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list-container {
overflow-y: auto; /* ?? 關(guān)鍵:overflow-y auto */
border: 1px solid #e8e8e8;
border-radius: 8px;
}
</style>
使用示例
<script setup>
import { ref } from 'vue'
import DynamicHeightVirtualList from './components/DynamicHeightVirtualList.vue'
// 數(shù)據(jù)量少
const fewItems = ref(
Array.from({ length: 4 }, (_, index) => ({
id: index + 1,
name: `項目 ${index + 1}`
}))
)
// 數(shù)據(jù)量多
const manyItems = ref(
Array.from({ length: 1000 }, (_, index) => ({
id: index + 1,
name: `項目 ${index + 1}`
}))
)
</script>
<template>
<!-- 數(shù)據(jù)少時,容器高度 = 4 * 50 = 200px -->
<DynamicHeightVirtualList
:items="fewItems"
:item-height="50"
:max-height="400"
/>
<!-- 數(shù)據(jù)多時,容器高度 = max-height = 400px,出現(xiàn)滾動條 -->
<DynamicHeightVirtualList
:items="manyItems"
:item-height="50"
:max-height="400"
/>
</template>
關(guān)鍵差異對比
| 屬性 | 固定容器 | 動態(tài)容器 |
|---|---|---|
| 容器樣式 | height: 400px | max-height: 400px |
| overflow | overflow: hidden | overflow-y: auto |
| 數(shù)據(jù)少時 | 仍占據(jù) 400px | 自動縮小 |
| 數(shù)據(jù)多時 | 顯示滾動條 | 顯示滾動條 |
優(yōu)點
- 容器高度自適應,更靈活
- 數(shù)據(jù)少時不浪費空間
- 性能依然優(yōu)秀
缺點
- 需要正確處理 overflow-y
- 布局可能因高度變化而跳動
動態(tài)項高度 + 固定容器高度
方案特點
- 項目高度: 動態(tài)計算(通過函數(shù))
- 容器高度: 固定(如 450px)
- 適用場景: 多樣化內(nèi)容、卡片列表、復雜布局
適用場景
| 場景 | 說明 |
|---|---|
| 新聞列表 | 標題長度不一,內(nèi)容預覽不同 |
| 卡片流 | 不同類型的卡片高度不同 |
| 評論列表 | 評論內(nèi)容長度不一 |
| 商品展示 | 不同商品信息復雜度不同 |
| 郵件列表 | 帶附件、標簽等額外信息 |
完整實現(xiàn)代碼
組件代碼: VariableHeightVirtualList.vue
<script setup>
import { ref } from "vue";
import { useVirtualList } from "@vueuse/core";
const props = defineProps({
items: {
type: Array,
default: () => [],
required: true,
},
containerHeight: {
type: Number,
default: 400,
},
});
const container = ref(null);
// ?? 關(guān)鍵:動態(tài)計算每個項的高度
const getItemHeight = (index) => {
// 方式 1: 根據(jù)索引模式
const heightPattern = index % 3;
if (heightPattern === 0) return 60; // 小項
if (heightPattern === 1) return 80; // 中項
return 100; // 大項
// 方式 2: 根據(jù)數(shù)據(jù)內(nèi)容
// const item = props.items[index];
// return item.type === 'large' ? 100 : 50;
// 方式 3: 根據(jù)文本長度
// const textLength = props.items[index].text.length;
// return Math.max(50, Math.ceil(textLength / 20) * 30);
};
// ?? 關(guān)鍵配置:itemHeight 為函數(shù)
const { list, containerProps, wrapperProps } = useVirtualList(props.items, {
itemHeight: getItemHeight, // 函數(shù),不是數(shù)值!
overscan: 5,
});
</script>
<template>
<div
ref="container"
class="virtual-list-container"
:style="{ height: `${containerHeight}px` }"
v-bind="containerProps"
>
<div class="virtual-list-wrapper" v-bind="wrapperProps">
<div
v-for="item in list"
:key="item.index"
class="virtual-list-item"
:style="{ height: `${getItemHeight(item.index)}px` }" <!-- 動態(tài)高度 -->
:data-height="getItemHeight(item.index)"
>
<div class="item-content">
<span class="item-index">{{ item.data.id }}</span>
<div class="item-info">
<span class="item-text">{{ item.data.name }}</span>
<span class="item-height-tag">高度: {{ getItemHeight(item.index) }}px</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list-container {
overflow: hidden;
}
/* 根據(jù)高度添加不同的視覺樣式 */
.virtual-list-item[data-height="60"] {
background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%);
}
.virtual-list-item[data-height="80"] {
background: linear-gradient(135deg, #e6f7ff 0%, #ffffff 100%);
}
.virtual-list-item[data-height="100"] {
background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%);
}
</style>
使用示例
<script setup>
import { ref } from 'vue'
import VariableHeightVirtualList from './components/VariableHeightVirtualList.vue'
const items = ref(
Array.from({ length: 400 }, (_, index) => ({
id: index + 1,
name: `項目 ${index + 1}`
}))
)
</script>
<template>
<VariableHeightVirtualList
:items="items"
:container-height="450"
/>
</template>
高度計算函數(shù)示例
// 示例 1: 基于索引的規(guī)律模式
const getItemHeight = (index) => {
return 50 + (index % 3) * 25; // 50, 75, 100 循環(huán)
};
// 示例 2: 基于數(shù)據(jù)類型
const getItemHeight = (index) => {
const item = props.items[index];
switch(item.type) {
case 'header': return 80;
case 'content': return 120;
case 'footer': return 60;
default: return 50;
}
};
// 示例 3: 基于內(nèi)容長度
const getItemHeight = (index) => {
const item = props.items[index];
const textLength = item.description?.length || 0;
const lines = Math.ceil(textLength / 40); // 假設(shè)每行 40 字符
return 50 + (lines - 1) * 20; // 基礎(chǔ)高度 + 額外行高度
};
// 示例 4: 基于屬性組合
const getItemHeight = (index) => {
const item = props.items[index];
let height = 60; // 基礎(chǔ)高度
if (item.hasImage) height += 200;
if (item.hasTags) height += 30;
if (item.hasActions) height += 40;
return height;
};
重要注意事項
- 高度必須可預測:
getItemHeight(index)對同一個 index 必須始終返回相同的值 - 性能考慮: 函數(shù)會被頻繁調(diào)用,避免復雜計算
- 實際高度匹配: CSS 實際高度必須與計算高度一致
// ? 錯誤:隨機高度(不可預測)
const getItemHeight = (index) => {
return 50 + Math.random() * 50; // 每次調(diào)用結(jié)果不同!
};
// ? 錯誤:異步計算
const getItemHeight = async (index) => {
const data = await fetchData(index); // 不支持異步!
return data.height;
};
// ? 正確:可預測、同步
const getItemHeight = (index) => {
return props.items[index].preCalculatedHeight; // 提前計算好的高度
};
優(yōu)點
- 支持不同高度的項目
- 容器高度固定,布局穩(wěn)定
- 視覺效果更豐富
缺點
- 高度必須提前可計算
- 不支持真正的動態(tài)內(nèi)容(如圖片加載后才知道高度)
- 計算開銷略高于固定高度
動態(tài)項高度 + 動態(tài)容器高度
方案特點
- 項目高度: 動態(tài)計算(通過函數(shù))
- 容器高度: 動態(tài)自適應(max-height)
- 適用場景: 最靈活的場景,結(jié)合前三種優(yōu)勢
適用場景
| 場景 | 說明 |
|---|---|
| 復雜彈窗 | 彈窗內(nèi)的復雜列表,高度不確定 |
| 搜索結(jié)果 | 不同類型的搜索結(jié)果混合 |
| 通知中心 | 不同類型通知,數(shù)量動態(tài)變化 |
| 購物車 | 不同商品信息,數(shù)量動態(tài) |
| 活動頁面 | 不同類型的活動卡片 |
完整實現(xiàn)代碼
組件代碼: FullyDynamicVirtualList.vue
<script setup>
import { ref } from "vue";
import { useVirtualList } from "@vueuse/core";
const props = defineProps({
items: {
type: Array,
default: () => [],
required: true,
},
maxHeight: {
type: Number,
default: 500,
},
});
const container = ref(null);
// ?? 關(guān)鍵:動態(tài)計算每個項的高度
const getItemHeight = (index) => {
const heightPattern = index % 3;
if (heightPattern === 0) return 60;
if (heightPattern === 1) return 80;
return 100;
};
// ?? 關(guān)鍵配置:itemHeight 為函數(shù) + 容器使用 max-height
const { list, containerProps, wrapperProps } = useVirtualList(props.items, {
itemHeight: getItemHeight,
overscan: 5,
});
</script>
<template>
<div
ref="container"
class="virtual-list-container"
:style="{ maxHeight: `${maxHeight}px` }" <!-- ?? max-height 動態(tài)容器 -->
v-bind="containerProps"
>
<div class="virtual-list-wrapper" v-bind="wrapperProps">
<div
v-for="item in list"
:key="item.index"
class="virtual-list-item"
:style="{ height: `${getItemHeight(item.index)}px` }"
:data-height="getItemHeight(item.index)"
>
<div class="item-content">
<span class="item-index">{{ item.data.id }}</span>
<div class="item-info">
<span class="item-text">{{ item.data.name }}</span>
<span class="item-height-tag">高度: {{ getItemHeight(item.index) }}px</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list-container {
overflow-y: auto; /* ?? 關(guān)鍵:overflow-y auto */
border: 1px solid #e8e8e8;
border-radius: 8px;
}
</style>
使用示例
<script setup>
import { ref } from 'vue'
import FullyDynamicVirtualList from './components/FullyDynamicVirtualList.vue'
const items = ref(
Array.from({ length: 200 }, (_, index) => ({
id: index + 1,
name: `項目 ${index + 1}`
}))
)
</script>
<template>
<FullyDynamicVirtualList
:items="items"
:max-height="600"
/>
</template>
適配不同數(shù)據(jù)量
<template>
<!-- 數(shù)據(jù)少:容器自動縮小 -->
<FullyDynamicVirtualList
:items="[1, 2, 3]"
:max-height="400"
/>
<!-- 實際高度約: 60 + 80 + 100 = 240px -->
<!-- 數(shù)據(jù)多:容器達到最大高度,顯示滾動條 -->
<FullyDynamicVirtualList
:items="Array(200)"
:max-height="400"
/>
<!-- 實際高度: 400px (max-height) -->
</template>
優(yōu)點
- 最大的靈活性
- 項目高度可變
- 容器高度自適應
- 適應各種復雜場景
缺點
- 實現(xiàn)相對復雜
- 需要同時處理兩種動態(tài)性
- 性能略低于前三種方案
性能對比與最佳實踐
性能對比
| 方案 | 性能 | 靈活性 | 實現(xiàn)難度 | 適用場景 |
|---|---|---|---|---|
| 固定+固定 | ????? | ?? | ? | 標準列表 |
| 固定+動態(tài) | ????? | ??? | ?? | 響應式布局 |
| 動態(tài)+固定 | ???? | ???? | ??? | 多樣化內(nèi)容 |
| 動態(tài)+動態(tài) | ???? | ????? | ???? | 復雜場景 |
渲染效率對比
測試條件: 10,000 條數(shù)據(jù)
| 方案 | DOM 節(jié)點數(shù) | 首次渲染時間 | 滾動 FPS | 內(nèi)存占用 |
|---|---|---|---|---|
| 傳統(tǒng)列表 | 10,000 | ~2000ms | <30 FPS | ~50MB |
| 虛擬列表(固定+固定) | ~15 | ~50ms | 60 FPS | ~5MB |
| 虛擬列表(固定+動態(tài)) | ~15 | ~50ms | 60 FPS | ~5MB |
| 虛擬列表(動態(tài)+固定) | ~15 | ~60ms | 55-60 FPS | ~5MB |
| 虛擬列表(動態(tài)+動態(tài)) | ~15 | ~60ms | 55-60 FPS | ~5MB |
方案選擇決策樹
開始
│
├─ 所有項目高度是否相同?
│ ├─ 是 ──> 容器高度是否固定?
│ │ ├─ 是 ──> 【方案1: 固定+固定】最優(yōu)性能 ?????
│ │ └─ 否 ──> 【方案2: 固定+動態(tài)】響應式布局 ?????
│ │
│ └─ 否 ──> 容器高度是否固定?
│ ├─ 是 ──> 【方案3: 動態(tài)+固定】多樣化內(nèi)容 ????
│ └─ 否 ──> 【方案4: 動態(tài)+動態(tài)】最大靈活性 ????
最佳實踐
overscan 參數(shù)調(diào)優(yōu)
// 基礎(chǔ)配置 overscan: 5 // 默認值,適合大多數(shù)場景 // 快速滾動場景 overscan: 10 // 增加預渲染,防止白屏 // 性能敏感場景 overscan: 3 // 減少預渲染,降低開銷 // 移動端 overscan: 8 // 移動端滾動更快,建議增加
高度計算緩存
// ? 不推薦:每次都計算
const getItemHeight = (index) => {
const item = props.items[index];
return calculateComplexHeight(item); // 復雜計算
};
// ? 推薦:緩存計算結(jié)果
const heightCache = new Map();
const getItemHeight = (index) => {
if (heightCache.has(index)) {
return heightCache.get(index);
}
const item = props.items[index];
const height = calculateComplexHeight(item);
heightCache.set(index, height);
return height;
};
// 數(shù)據(jù)變化時清除緩存
watch(() => props.items, () => {
heightCache.clear();
}, { deep: true });
響應式優(yōu)化
// ? 不推薦:直接傳入響應式對象 const items = reactive([...]); // ? 推薦:使用 ref const items = ref([...]); // ? 更好:使用 shallowRef(大數(shù)據(jù)量) const items = shallowRef([...]);
大數(shù)據(jù)量優(yōu)化
// 數(shù)據(jù)量 > 50,000 時的優(yōu)化策略
// 1. 使用 shallowRef
const items = shallowRef(largeDataArray);
// 2. 增加 overscan
overscan: 15
// 3. 防抖滾動事件(如果需要自定義處理)
const handleScroll = useDebounceFn(() => {
// 處理滾動
}, 16); // 約 60fps
// 4. 虛擬滾動條(可選)
// 使用自定義滾動條替代原生滾動條
避免常見錯誤
// ? 錯誤 1:在模板中直接訪問原數(shù)組
<template>
<div v-for="item in items"> <!-- 錯誤!應該用 list -->
{{ item }}
</div>
</template>
// ? 正確:使用 list
<template>
<div v-for="item in list"> <!-- 正確!-->
{{ item.data }}
</div>
</template>
// ? 錯誤 2:忘記綁定 props
<div class="container"> <!-- 錯誤!缺少 v-bind -->
<div class="wrapper"> <!-- 錯誤!缺少 v-bind -->
// ? 正確:綁定 containerProps 和 wrapperProps
<div v-bind="containerProps">
<div v-bind="wrapperProps">
// ? 錯誤 3:高度計算不準確
.item {
height: 50px;
padding: 10px; /* 實際高度 = 50 + 20 = 70px */
}
// ? 正確:確保計算高度包含 padding/border
itemHeight: 70 // 或使用 box-sizing: border-box
常見問題與解決方案
滾動位置跳動
問題: 滾動時列表位置跳動
原因: 計算高度與實際高度不匹配
解決方案:
// 1. 使用 box-sizing: border-box
.virtual-list-item {
box-sizing: border-box;
height: 50px; /* 包含 padding 和 border */
padding: 10px;
}
// 2. 確保高度計算準確
const getItemHeight = (index) => {
const baseHeight = 50;
const padding = 20; // top + bottom
const border = 2; // top + bottom
return baseHeight + padding + border;
};
首次渲染白屏
問題: 首次加載時短暫白屏
原因: overscan 太小或數(shù)據(jù)加載慢
解決方案:
// 1. 增加 overscan
overscan: 10
// 2. 添加骨架屏
<template>
<div v-if="loading">
<SkeletonItem v-for="i in 10" :key="i" />
</div>
<div v-else v-bind="containerProps">
<!-- 虛擬列表 -->
</div>
</template>
// 3. 預加載數(shù)據(jù)
onMounted(async () => {
loading.value = true;
items.value = await fetchData();
loading.value = false;
});
滾動到指定位置
問題: 如何滾動到指定索引的項目
解決方案:
// 方法 1: 計算滾動位置(固定高度)
const scrollToIndex = (index) => {
const scrollTop = index * itemHeight;
container.value.scrollTop = scrollTop;
};
// 方法 2: 計算滾動位置(動態(tài)高度)
const scrollToIndex = (index) => {
let scrollTop = 0;
for (let i = 0; i < index; i++) {
scrollTop += getItemHeight(i);
}
container.value.scrollTop = scrollTop;
};
// 方法 3: 使用 scrollIntoView
const scrollToIndex = (index) => {
// 需要在 list 中找到對應的元素
const element = document.querySelector(`[data-index="${index}"]`);
element?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
數(shù)據(jù)更新后滾動位置重置
問題: 數(shù)據(jù)更新后滾動位置跳回頂部
原因: items 引用變化導致重新計算
解決方案:
// ? 錯誤:直接賦值新數(shù)組
items.value = newData; // 引用變化,滾動位置重置
// ? 正確:保持引用,修改內(nèi)容
// 方法 1: 使用 splice
items.value.splice(0, items.value.length, ...newData);
// 方法 2: 逐項更新
newData.forEach((item, index) => {
items.value[index] = item;
});
// 方法 3: 記錄并恢復滾動位置
const scrollTop = container.value.scrollTop;
items.value = newData;
nextTick(() => {
container.value.scrollTop = scrollTop;
});
在 TypeScript 中使用
import { ref, Ref } from 'vue'
import { useVirtualList, UseVirtualListOptions } from '@vueuse/core'
interface ListItem {
id: number
name: string
description?: string
}
const items: Ref<ListItem[]> = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
])
const options: UseVirtualListOptions = {
itemHeight: 50,
overscan: 5
}
const { list, containerProps, wrapperProps } = useVirtualList(items, options)
// list 的類型是 ComputedRef<{ index: number; data: ListItem }[]>
服務端渲染 (SSR) 支持
// 在 SSR 環(huán)境中,需要注意以下幾點:
// 1. 容器高度必須明確
<div :style="{ height: '400px' }"> <!-- SSR 需要明確高度 -->
// 2. 避免在服務端計算滾動
import { onMounted } from 'vue'
const setupVirtualList = () => {
if (typeof window === 'undefined') return // SSR 環(huán)境跳過
const { list, containerProps, wrapperProps } = useVirtualList(...)
return { list, containerProps, wrapperProps }
}
// 3. 使用 ClientOnly 組件(Nuxt.js)
<ClientOnly>
<VirtualList :items="items" />
</ClientOnly>
總結(jié)與對比表
四種方案完整對比
| 特性 | 固定+固定 | 固定+動態(tài) | 動態(tài)+固定 | 動態(tài)+動態(tài) |
|---|---|---|---|---|
| 項目高度 | 固定值 | 固定值 | 函數(shù)計算 | 函數(shù)計算 |
| 容器高度 | height | max-height | height | max-height |
| overflow | hidden | auto | hidden | auto |
| itemHeight | 50 | 50 | (i) => ... | (i) => ... |
| 性能評分 | 5/5 | 5/5 | 4/5 | 4/5 |
| 靈活性評分 | 2/5 | 3/5 | 4/5 | 5/5 |
| 實現(xiàn)難度 | 簡單 | 簡單 | 中等 | 中等 |
| 典型應用 | 表格 | 下拉菜單 | 卡片列表 | 復雜彈窗 |
| 數(shù)據(jù)量建議 | 無限制 | 無限制 | < 100,000 | < 100,000 |
API 參數(shù)速查表
// useVirtualList 配置
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 50 | ((index: number) => number), // 必需
overscan: 5 // 可選,默認 5
})
關(guān)鍵代碼片段速查
固定高度配置
itemHeight: 50
動態(tài)高度配置
itemHeight: (index) => {
return index % 2 === 0 ? 60 : 80
}
固定容器
<div :style="{ height: '400px' }" v-bind="containerProps">
動態(tài)容器
<div :style="{ maxHeight: '400px' }" v-bind="containerProps">
完整模板結(jié)構(gòu)
<div v-bind="containerProps" :style="{ height: '400px' }">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index">
{{ item.data }}
</div>
</div>
</div>
完整項目結(jié)構(gòu)
vue3-sample/
├── src/
│ ├── components/
│ │ ├── FixedHeightVirtualList.vue # 固定+固定
│ │ ├── DynamicHeightVirtualList.vue # 固定+動態(tài)
│ │ ├── VariableHeightVirtualList.vue # 動態(tài)+固定
│ │ └── FullyDynamicVirtualList.vue # 動態(tài)+動態(tài)
│ ├── App.vue # 使用示例
│ └── main.js
├── docs/
│ └── virtual-list-guide.md # 本文檔
├── package.json
└── README.md
快速開始
# 1. 克隆項目 git clone https://github.com/ilcherry/virtuallist-examples # 2. 安裝依賴 cd vue3-sample pnpm install # 3. 運行開發(fā)服務器 pnpm dev # 4. 訪問 # 打開瀏覽器訪問 http://localhost:5173

以上就是Vue中實現(xiàn)虛擬列表的4種技術(shù)方案調(diào)研的詳細內(nèi)容,更多關(guān)于Vue虛擬列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+springboot+element+vue-resource實現(xiàn)文件上傳教程
這篇文章主要介紹了vue+springboot+element+vue-resource實現(xiàn)文件上傳教程,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
vue init webpack myproject構(gòu)建項目 ip不能訪問的解決方法
下面小編就為大家分享一篇vue init webpack myproject構(gòu)建項目 ip不能訪問的解決方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03
vue-drawer-layout實現(xiàn)手勢滑出菜單欄
這篇文章主要為大家詳細介紹了vue-drawer-layout實現(xiàn)手勢滑出菜單欄,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11

