Vue3虛擬列表的實(shí)現(xiàn)原理與實(shí)戰(zhàn)指南
前言
在日常開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到需要展示大量數(shù)據(jù)的場(chǎng)景,比如展示幾千條甚至上萬(wàn)條數(shù)據(jù)列表。如果直接將所有數(shù)據(jù)渲染到頁(yè)面中,會(huì)導(dǎo)致嚴(yán)重的性能問(wèn)題,頁(yè)面會(huì)變得卡頓甚至崩潰。虛擬列表(Virtual List)正是為了解決這一問(wèn)題而誕生的技術(shù)。
本文將介紹虛擬列表的核心原理,并基于 Vue3 手寫(xiě)一個(gè)完整的虛擬列表組件。
一、什么是虛擬列表
虛擬列表是一種按需渲染的技術(shù),它只渲染當(dāng)前可視區(qū)域內(nèi)的列表項(xiàng),而不是一次性渲染所有數(shù)據(jù)。想象一下,你有一本1000頁(yè)的書(shū),但你在任何時(shí)候只能看到其中的一頁(yè)。虛擬列表的工作方式就和翻書(shū)一樣——只展示當(dāng)前"看得到"的內(nèi)容。
虛擬列表的核心優(yōu)勢(shì)
| 優(yōu)勢(shì) | 說(shuō)明 |
|---|---|
| 性能提升 | 只渲染可見(jiàn)區(qū)域的數(shù)據(jù),DOM 節(jié)點(diǎn)數(shù)量大幅減少 |
| 內(nèi)存優(yōu)化 | 避免創(chuàng)建大量 DOM 節(jié)點(diǎn),降低內(nèi)存占用 |
| 流暢體驗(yàn) | 滾動(dòng)更加流暢,不會(huì)因?yàn)閿?shù)據(jù)量大而卡頓 |
| 快速首屏 | 首屏渲染時(shí)間大大縮短 |
二、虛擬列表的實(shí)現(xiàn)原理
虛擬列表的核心思想其實(shí)很簡(jiǎn)單,可以用以下幾個(gè)步驟來(lái)概括:
- 計(jì)算可視區(qū)域:根據(jù)滾動(dòng)位置和容器高度,計(jì)算出當(dāng)前可見(jiàn)的數(shù)據(jù)范圍
- 按需渲染:只渲染計(jì)算出的可見(jiàn)數(shù)據(jù),而不是全部數(shù)據(jù)
- 占位模擬:通過(guò) padding 或 transform 模擬完整的滾動(dòng)高度,讓滾動(dòng)條看起來(lái)正常
下面這張圖清晰地展示了虛擬列表的工作原理:
┌─────────────────────────────┐ │ 已渲染區(qū)域(可見(jiàn)) │ │ ┌─────────────────────┐ │ │ │ Item 3 │ │ │ │ Item 4 │ │ │ │ Item 5 │ │ ← 當(dāng)前可見(jiàn)區(qū)域 │ │ Item 6 │ │ │ │ Item 7 │ │ │ └─────────────────────┘ │ ├─────────────────────────────┤ │ 未渲染區(qū)域(不可見(jiàn)) │ │ ... Item 8-100 │ └─────────────────────────────┘
三、代碼實(shí)現(xiàn)
子組件:MyList.vue
<template>
<div style="overflow-y:auto ;border:3px solid blue;position: relative;" :style="{ height:props.size * 10 + 'px'}" @scroll="handSrool">
<ul :style="{ height:(props.size+1) * props.testData.length + 'px', transform: `translateY(${offsetY}px)` }" >
<li v-for="(item, index) in list" class="list-item" :style="{ height:props.size + 'px', position: 'absolute', top: (index * props.size) + 'px', width: '100%' }">
{{ item }}
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue';
// 接受父節(jié)點(diǎn)參數(shù)
const props = defineProps({
testData: Array,
size: {
type: Number,
default: '6'
}
})
const startIndex = ref(0)
const endIndex = computed(() => startIndex.value + 10)
// 計(jì)算偏移量,讓列表項(xiàng)跟隨滾動(dòng)
const offsetY = computed(() => startIndex.value * props.size)
const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))
// 監(jiān)聽(tīng)滾動(dòng)
const handSrool = (e) => {
// 獲取滾動(dòng)距離
const scrollTop = e.target.scrollTop
// 根據(jù)滾動(dòng)距離計(jì)算起始索引
startIndex.value = Math.floor(scrollTop / props.size)
}
onMounted(() => {
console.log(props.size)
console.log(endIndex.value)
})
</script>
<style>
.list-item {
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid rgb(146, 144, 144);
}
</style>父組件:使用示例
<script setup>
import MyList from './components/MyList.vue';
const data = []
// 模擬數(shù)據(jù)
for (let i = 0; i < 100; i++) {
data.push({
id: i,
name: `name${i}`,
age: i,
sex: i % 2 === 0 ? '男' : '女',
address: `address${i}`,
date: new Date(),
})
}
</script>
<template>
<div class="table_continue">
<MyList :testData="data" :size="60"/>
</div>
</template>
<style scoped>
.table_continue {
padding: 20px;
}
</style>四、核心實(shí)現(xiàn)解析
1. 計(jì)算可見(jiàn)區(qū)域的起始索引
const handSrool = (e) => {
const scrollTop = e.target.scrollTop
startIndex.value = Math.floor(scrollTop / props.size)
}這是虛擬列表最核心的邏輯。當(dāng)用戶(hù)滾動(dòng)時(shí),我們通過(guò) scrollTop 獲取滾動(dòng)距離,然后除以單個(gè)列表項(xiàng)的高度,就能計(jì)算出當(dāng)前應(yīng)該從第幾條數(shù)據(jù)開(kāi)始渲染。
為什么要這樣做?
假設(shè)每個(gè)列表項(xiàng)高度是 60px,滾動(dòng)位置是 300px,那么 300 / 60 = 5,說(shuō)明用戶(hù)已經(jīng)滾過(guò)了5個(gè)列表項(xiàng),所以我們應(yīng)該從第5條數(shù)據(jù)開(kāi)始渲染。
2. 計(jì)算可見(jiàn)數(shù)據(jù)
const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))
使用 slice 方法截取數(shù)組中的一部分,只渲染從 startIndex 開(kāi)始的 10 條數(shù)據(jù)。這樣無(wú)論原數(shù)組有多大,DOM 中最多只有 10 個(gè)列表項(xiàng)元素。
3. 使用 transform 模擬滾動(dòng)
const offsetY = computed(() => startIndex.value * props.size)
這是實(shí)現(xiàn)無(wú)縫滾動(dòng)的關(guān)鍵。雖然我們只渲染了 10 條數(shù)據(jù),但通過(guò) transform: translateY() 將整個(gè)列表向下偏移,偏移量等于被"跳過(guò)"的數(shù)據(jù)的總高度。這樣用戶(hù)看起來(lái)就像在滾動(dòng)整個(gè)列表一樣。
4. 占位容器的高度
:style="{ height:(props.size+1) * props.testData.length + 'px' }"
外層容器的高度等于單個(gè)列表項(xiàng)高度乘以總數(shù)據(jù)條數(shù),這就是讓滾動(dòng)條看起來(lái)正常的"秘密"。瀏覽器會(huì)根據(jù)這個(gè)高度生成相應(yīng)長(zhǎng)度的滾動(dòng)條,用戶(hù)拖動(dòng)滾動(dòng)條時(shí)就會(huì)觸發(fā) scroll 事件。
五、性能優(yōu)化建議
雖然上面的實(shí)現(xiàn)已經(jīng)能夠正常工作,但在生產(chǎn)環(huán)境中,你可能還需要考慮以下優(yōu)化點(diǎn):
1. 預(yù)渲染更多數(shù)據(jù)
可以在可見(jiàn)區(qū)域的前后各多渲染幾條數(shù)據(jù),避免快速滾動(dòng)時(shí)出現(xiàn)空白:
const BUFFER = 3
const endIndex = computed(() => startIndex.value + 10 + BUFFER)
const list = computed(() => {
const start = Math.max(0, startIndex.value - BUFFER)
return props.testData.slice(start, endIndex.value)
})
2. 使用防抖處理滾動(dòng)事件
如果數(shù)據(jù)量非常大,可以使用防抖來(lái)減少計(jì)算頻率:
import { debounce } from 'lodash-es'
const handSrool = debounce((e) => {
const scrollTop = e.target.scrollTop
startIndex.value = Math.floor(scrollTop / props.size)
}, 16)
3. 處理列表項(xiàng)高度不一致的情況
在實(shí)際應(yīng)用中,列表項(xiàng)的高度可能是動(dòng)態(tài)的。這時(shí)可以使用測(cè)量行高的方案,或者將所有列表項(xiàng)設(shè)為固定高度。
4. 使用 CSS will-change 優(yōu)化動(dòng)畫(huà)
.list-item {
will-change: transform;
}
這告訴瀏覽器該元素會(huì)發(fā)生變化,可以提前進(jìn)行優(yōu)化。
六、原理圖解
讓我們通過(guò)一張圖來(lái)理解整個(gè)虛擬列表的工作流程:
用戶(hù)滾動(dòng) → 獲取 scrollTop → 計(jì)算 startIndex → slice 取數(shù)據(jù) → translateY 偏移
│ │
↓ ↓
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌────────────┐
│ 滾動(dòng)事件 │ → │ scrollTop │ → │ 300/60=5 │ → │ 取第5-15條 │
│ 觸發(fā) │ │ = 300px │ │ start=5 │ │ translateY │
└─────────┘ └─────────────┘ └──────────┘ │ = 300px │
└────────────┘
七、總結(jié)
虛擬列表是前端性能優(yōu)化中非常重要的技術(shù)之一。它的核心思想可以用一句話(huà)概括:只渲染可見(jiàn)的內(nèi)容,用占位符模擬完整的滾動(dòng)高度。
通過(guò)本文的實(shí)現(xiàn),我們可以看到一個(gè)基礎(chǔ)的虛擬列表組件并不復(fù)雜,主要涉及到以下幾個(gè)關(guān)鍵點(diǎn):
- 監(jiān)聽(tīng)滾動(dòng)事件:獲取用戶(hù)的滾動(dòng)位置
- 計(jì)算可見(jiàn)索引:根據(jù)滾動(dòng)位置計(jì)算應(yīng)該顯示的數(shù)據(jù)范圍
- 按需渲染:只渲染計(jì)算范圍內(nèi)的數(shù)據(jù)
- 模擬滾動(dòng):通過(guò) transform 或 padding 模擬完整的滾動(dòng)高度
掌握了這些核心原理后,你就可以在此基礎(chǔ)上進(jìn)行各種定制化開(kāi)發(fā),比如支持動(dòng)態(tài)高度的列表項(xiàng)、添加加載更多功能、實(shí)現(xiàn)分組虛擬列表等。
以上就是Vue3虛擬列表的實(shí)現(xiàn)原理與實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于Vue3虛擬列表實(shí)現(xiàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue router返回到指定的路由的場(chǎng)景分析
這篇文章主要介紹了vue router返回到指定的路由的場(chǎng)景分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11
Vue中Vue-Baidu-Map基本使用方法實(shí)例
最近有一個(gè)項(xiàng)目需要用到地圖來(lái)展示位置并進(jìn)行數(shù)據(jù)交互,用vue-baidu-map實(shí)現(xiàn)出來(lái),下面這篇文章主要給大家介紹了關(guān)于Vue中Vue-Baidu-Map基本使用的相關(guān)資料,需要的朋友可以參考下2023-03-03
在Vue3和TypeScript中大文件分片上傳的實(shí)現(xiàn)與優(yōu)化
本文介紹在 Vue 3 和 TypeScript 環(huán)境下大文件分片上傳的實(shí)現(xiàn)與優(yōu)化,包括項(xiàng)目前后端技術(shù)棧,前端的文件切片、并發(fā)上傳、計(jì)算 Hash、斷點(diǎn)續(xù)傳和用戶(hù)體驗(yàn)優(yōu)化,后端的文件接收存儲(chǔ)、切片合并、異常處理與日志記錄,還提及遇到的問(wèn)題及解決方案,總結(jié)了此方式的優(yōu)勢(shì)和重要性2025-01-01
vue中computed和watch的使用實(shí)例代碼解析
這篇文章主要介紹了vue中computed和watch的綜合運(yùn)用實(shí)例,主要需求是點(diǎn)擊按鈕實(shí)現(xiàn)天氣的切換效果結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
vue如何處理base64格式文件pdf及圖片預(yù)覽功能
這篇文章主要給大家介紹了關(guān)于vue如何處理base64格式文件pdf及圖片預(yù)覽功能的相關(guān)資料,圖片的base64編碼就是可以將一副圖片數(shù)據(jù)編碼成一串字符串,使用該字符串代替圖像地址,需要的朋友可以參考下2024-05-05
vue改變對(duì)象或數(shù)組時(shí)的刷新機(jī)制的方法總結(jié)
這篇文章主要介紹了vue改變對(duì)象或數(shù)組時(shí)的刷新機(jī)制的方法總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04

