vue實(shí)現(xiàn)不定高虛擬列表的示例詳解
虛擬列表主要解決大數(shù)據(jù)量數(shù)據(jù)一次渲染性能差的問(wèn)題。
之前寫過(guò)一篇關(guān)于虛擬列表實(shí)現(xiàn)的文章:造輪子之不同場(chǎng)景下虛擬列表實(shí)現(xiàn),主要講了定高(高度統(tǒng)一和高度不統(tǒng)一兩種情況)虛擬列表的實(shí)現(xiàn),本文著重研究不定高虛擬列表的實(shí)現(xiàn)。在vue環(huán)境單頁(yè)面項(xiàng)目下研究實(shí)現(xiàn)。
前文講過(guò)虛擬列表的要做的事是確保性能的前提下,利用一定的技術(shù)模擬全數(shù)據(jù)一次性渲染后效果。
定高虛擬列表原理

綠色部分為containter,也就是父容器,它會(huì)有固定的高度。黃色部分為content,它是父容器的子元素。
當(dāng)content的高度超過(guò)父容器的高度,就可以滾動(dòng)內(nèi)容區(qū)了,這就是一般滾動(dòng)原理。
虛擬列表需要使用這個(gè)滾動(dòng)原理。虛擬列表使用占位div,設(shè)置占位div的高度為所有列表數(shù)據(jù)的高度進(jìn)而撐開containter,形成滾動(dòng)條。
然后虛擬列表具體渲染過(guò)程中,只是渲染可視區(qū)也就是父容器區(qū)域

至于可視區(qū)域的內(nèi)容滾動(dòng)通過(guò)監(jiān)聽滾動(dòng)條scroll事件,獲取到滾動(dòng)距離scrllTop,轉(zhuǎn)換為可視區(qū)域的偏移位置,同時(shí)獲取渲染數(shù)據(jù)的起始和結(jié)束索引,渲染指定段數(shù)據(jù)形成假象的滾動(dòng)。
不定高內(nèi)容數(shù)渲染
上一篇文章造輪子之不同場(chǎng)景下虛擬列表實(shí)現(xiàn)已經(jīng)給出了定高虛擬列表的實(shí)現(xiàn)。不定高相對(duì)定高的難點(diǎn)在于數(shù)據(jù)沒有渲染之前根本不知道數(shù)據(jù)的實(shí)際高度,解決方案理論上有
- 在屏幕外渲染,但消耗性能
- 以預(yù)估高度先行渲染,然后獲取真實(shí)高度并緩存
采用第一種方案顯然是不完美的,所以采用第二個(gè)方案,這也是之前有人實(shí)現(xiàn)過(guò)的。
不定高假數(shù)據(jù)
為了更接近業(yè)務(wù),這里使用vue-codemirror方式渲染數(shù)據(jù),為vue-codemirror造假數(shù)據(jù)
function generateRandomNumber () {
const min = 100
const max = 1000
// 生成隨機(jī)整數(shù)
const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min
return randomNumber
}
function getRandomLetter () {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const randomIndex = Math.floor(Math.random() * letters.length)
const randomLetter = letters.charAt(randomIndex)
return randomLetter
}
function generateString (length) {
const minLength = 100
const maxLength = 1000
// 確保長(zhǎng)度在最小和最大范圍內(nèi)
if (length < minLength) {
length = minLength
} else if (length > maxLength) {
length = maxLength
}
// 生成字符串
const string = getRandomLetter().repeat(length)
return string
}
const d = []
for (let i = 0; i < 500; i++) {
const length = generateRandomNumber()
d.push({
data: generateString(length),
index: i
})
}
這里造了500條,具體是隨機(jī)生成的字符串,字符串長(zhǎng)度100-1000,字符從A-Z中選取。
溫故定高虛擬列表
因?yàn)椴欢ǜ咛摂M列表有和定高虛擬列表相似之處,再來(lái)回顧一下之前定高(統(tǒng)一高度和不統(tǒng)一高度)的解決方案。這里只展示一下統(tǒng)一高度的,不統(tǒng)一高度的可以查看造輪子之不同場(chǎng)景下虛擬列表實(shí)現(xiàn)。統(tǒng)一高度組件代碼
<template>
<div ref="list" class="render-list-container" @scroll="scrollEvent($event)">
<!-- 占位div -->
<div class="render-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="render-list" :style="{ transform: getTransform }">
<template
v-for="item in visibleData"
>
<slot :value="item.value" :height="itemSize + 'px'" :index="item.id"></slot>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'VirtualList',
props: {
// 所有列表數(shù)據(jù)
listData: {
type: Array,
default: () => []
},
// 每項(xiàng)高度
itemSize: {
type: Number,
default: 100
}
},
computed: {
// 列表總高度
listHeight () {
return this.listData.length * this.itemSize
},
// 可顯示的列表項(xiàng)數(shù)
visibleCount () {
return Math.ceil(this.screenHeight / this.itemSize)
},
// 偏移量對(duì)應(yīng)的style
getTransform () {
return `translate3d(0,${this.startOffset}px,0)`
},
// 獲取真實(shí)顯示列表數(shù)據(jù)
visibleData () {
return this.listData.slice(this.start, Math.min(this.end, this.listData.length))
}
},
mounted () {
this.screenHeight = this.$el.clientHeight
this.end = this.start + this.visibleCount
},
data () {
return {
// 可視區(qū)域高度
screenHeight: 0,
// 偏移量
startOffset: 0,
// 起始索引
start: 0,
// 結(jié)束索引
end: null
}
},
methods: {
scrollEvent () {
// 當(dāng)前滾動(dòng)位置
const scrollTop = this.$refs.list.scrollTop
// 此時(shí)的開始索引
this.start = Math.floor(scrollTop / this.itemSize)
// 此時(shí)的結(jié)束索引
this.end = this.start + this.visibleCount
// 此時(shí)的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize)
}
}
}
</script>
<style scoped>
.render-list-container {
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
height: 200px;
}
.render-list-phantom {
position: absolute;
left: 0;
right: 0;
z-index: -1;
}
.render-list {
text-align: center;
}
</style>
研究不定高虛擬列表組件
按照統(tǒng)一高度方式渲染
正如上面所說(shuō)為了解決不定高內(nèi)容高度不定的問(wèn)題,采用
以預(yù)估高度先行渲染,然后獲取真實(shí)高度并緩存方案
所以給每條假數(shù)據(jù)一條預(yù)估高度,然后使用定高虛擬列表渲染數(shù)據(jù),渲染數(shù)據(jù)代碼
<template>
<div class="render-show">
<div>
<NoHasVirtualList :listData="data">
<template slot-scope="{ item, height }">
<codemirror
class="unit"
:style="{height: height}"
v-model="item.data"
:options="cmOptions"
></codemirror>
</template>
</NoHasVirtualList>
</div>
</div>
</template>
設(shè)置codemirror組件高度固定。查看一下效果

問(wèn)題很明顯,由于codemirror組件設(shè)置固定高度,導(dǎo)致渲染內(nèi)容擠到一起了,分不清哪個(gè)是哪個(gè)高度方向出現(xiàn)重合。所以預(yù)估高度不是這樣用的,預(yù)估高度的意義:它是一種高度占位,是一種占位是務(wù)必要修正的。
修正高度
為了修正這個(gè)高度,需要等待數(shù)據(jù)渲染后拿到真實(shí)高度,這個(gè)需求可以在vue生命周期函數(shù)updated實(shí)現(xiàn),也可以通過(guò)IntersectionObserver實(shí)現(xiàn)。本文采用updated實(shí)現(xiàn)。
修正高度不僅修正每一條數(shù)據(jù)的高度,因?yàn)橛脕?lái)?yè)纹鹂梢晠^(qū)域的占位div高度也是根據(jù)預(yù)估高度計(jì)算的,所以占位div高度也需要更新,然后還需要更新偏移量。
具體在updated里獲取真實(shí)元素大小,修改對(duì)應(yīng)的尺寸緩存;更新占位div高度(使用計(jì)算屬性實(shí)現(xiàn));更新真實(shí)偏移量。
updated () {
this.$nextTick(() => {
// 獲取真實(shí)元素大小,修改對(duì)應(yīng)的尺寸緩存
this.updateItemsSize()
// 更新真實(shí)偏移量
this.setStartOffset()
})
},
獲取數(shù)據(jù)實(shí)際高度,修改對(duì)應(yīng)尺寸緩存
創(chuàng)建計(jì)算屬性_listData拷貝列表數(shù)據(jù)。目的盡量不修改傳進(jìn)來(lái)的listData列表數(shù)據(jù),同時(shí)給渲染列表數(shù)據(jù)添加索引,實(shí)際是給渲染用的visibleCount添加唯一索引
computed: {
_listData () {
return this.listData.reduce((init, cur, index) => {
init.push({
// _轉(zhuǎn)換后的索引
_key: index,
value: cur
})
return init
}, [])
},
...
}
緩存每條數(shù)據(jù)的高度、以及數(shù)據(jù)坐標(biāo):用top和bottom標(biāo)記
// 初始化緩存
initPositions () {
this.positions = this._listData.map((d, index) => ({
index,
height: this.itemSize,
top: index * this.itemSize,
bottom: (index + 1) * this.itemSize
}))
},
上面計(jì)算屬性_listData以及緩存每條數(shù)據(jù)均是服務(wù)于這一步:獲取渲染數(shù)據(jù)實(shí)際高度,修改對(duì)應(yīng)數(shù)據(jù)緩存尺寸
// 獲取實(shí)際高度,修正內(nèi)容高度
updateItemsSize () {
const nodes = this.$refs.items
nodes.forEach((node) => {
// 獲取元素自身的屬性
const rect = node.getBoundingClientRect()
const height = rect.height
const index = +node.id // id就是_listData上的唯一索引
const oldHeight = this.positions[index].height
const dValue = oldHeight - height
// 存在差值
if (dValue) {
this.positions[index].bottom = this.positions[index].bottom - dValue
this.positions[index].height = height
this.positions[index].over = true // TODO
for (let k = index + 1; k < this.positions.length; k++) {
this.positions[k].top = this.positions[k - 1].bottom
this.positions[k].bottom = this.positions[k].bottom - dValue
}
}
})
},
更新列表總高度
獲取數(shù)據(jù)實(shí)際高度,修改對(duì)應(yīng)尺寸緩存目的之一是為了更新列表總高度
computed: {
...
// 列表總高度
listHeight () {
return this.positions[this.positions.length - 1].bottom
},
...
},
上述代碼中this.listHeight是一個(gè)計(jì)算屬性,是占位div的高度。
<template>
<div
ref="list"
class="infinite-list-container"
@scroll="scrollEvent($event)"
>
<!-- 占位div -->
<div ref="phantom" class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
...
</div>
</template>
更新真實(shí)偏移量
獲取數(shù)據(jù)實(shí)際高度,修改對(duì)應(yīng)尺寸緩存目的之二是為了更新真實(shí)偏移量。
借助this.positions數(shù)組數(shù)據(jù),通過(guò)設(shè)置this.startOffset,在傳導(dǎo)到計(jì)算屬性this.contentTransform更新偏移量
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<!-- 占位div -->
<div
class="infinite-list-phantom"
:style="{ height: listHeight + 'px' }"
></div>
<div
ref="content"
:style="{ transform: contentTransform }"
class="infinite-list"
>
....
</div>
</div>
</template>
...
computed: {
...
// 偏移量對(duì)應(yīng)的style
contentTransform () {
return `translateY(${this.startOffset}px)`
},
...
},
...
// 更新偏移量
setStartOffset () {
if (this.start >= 1) {
const size =
this.positions[this.start].top -
(this.positions[this.start - this.aboveCount]
? this.positions[this.start - this.aboveCount].top
: 0)
this.startOffset = this.positions[this.start - 1].bottom - size
} else {
this.startOffset = 0
}
}
滾動(dòng)事件
滾動(dòng)事件用以觸發(fā)更新
// 滾動(dòng)事件
scrollEvent () {
// 當(dāng)前滾動(dòng)位置
const scrollTop = this.$refs.list.scrollTop
// 更新滾動(dòng)狀態(tài)
// 排除不需要計(jì)算的情況
if (
scrollTop > this.anchorPoint.bottom ||
scrollTop < this.anchorPoint.top
) {
// 此時(shí)的開始索引
this.start = this.getStartIndex(scrollTop)
// 此時(shí)的結(jié)束索引
this.end = this.start + this.visibleCount
// 更新偏移量
this.setStartOffset()
}
}
其中this.anchorPoint是計(jì)算屬性
computed: {
...
anchorPoint () {
return this.positions.length ? this.positions[this.start] : null
}
...
},
上述代碼中之所以排除不需要計(jì)算的情況,需要解釋一下。
真實(shí)的滾動(dòng)就是滾動(dòng)條滾動(dòng)了多少,可視區(qū)就向上移動(dòng)多少。但虛擬滾動(dòng)不是。當(dāng)起始索引發(fā)生變化時(shí),渲染數(shù)據(jù)發(fā)生變化了,但渲染數(shù)據(jù)的高度不是連續(xù)的,所以需要?jiǎng)討B(tài)的設(shè)置偏移量。當(dāng)滾動(dòng)時(shí)起始索引不發(fā)生變化時(shí),因?yàn)閿?shù)據(jù)變化是連續(xù)的,此時(shí)可以什么也不做,滾動(dòng)顯示的內(nèi)容由瀏覽器控制。排除的部分就是索引沒發(fā)生變化的情況。
根據(jù)滾動(dòng)高度獲取起始索引方法this.getStartIndex
methods: {
...
// 獲取列表起始索引
getStartIndex (scrollTop = 0) {
// 二分法查找
return this.binarySearch(this.positions, scrollTop)
},
// 二分法查找 用于查找開始索引
binarySearch (list, value) {
let start = 0
let end = list.length - 1
let tempIndex = null
while (start <= end) {
const midIndex = parseInt((start + end) / 2)
const midValue = list[midIndex].bottom
if (midValue === value) {
return midIndex + 1
} else if (midValue < value) {
start = midIndex + 1
} else if (midValue > value) {
if (tempIndex === null || tempIndex > midIndex) {
tempIndex = midIndex
}
end = end - 1
}
}
return tempIndex
},
...
}
效果查看以及優(yōu)化

給滾動(dòng)增加緩沖,緩沖就是多渲染幾條,上方和下方渲染額外的數(shù)據(jù),比如前后多渲染2條。增加計(jì)算屬性aboveCount和belowCount,同時(shí)修改visibleData
computed: {
...
aboveCount () {
return Math.min(this.start, 2)
},
belowCount () {
return Math.min(this.listData.length - this.end, 2)
},
visibleData () {
const start = this.start - this.aboveCount
const end = this.end + this.belowCount
return this._listData.slice(start, end)
}
},
存在問(wèn)題
即便是給滾動(dòng)增加緩沖,過(guò)快滑動(dòng)時(shí)依然會(huì)出現(xiàn)白屏現(xiàn)象,究其本質(zhì)是滾動(dòng)過(guò)快而真實(shí)dom更新趕不上它

總結(jié)
本文主要研究了不定高虛擬列表的一種實(shí)現(xiàn)?;驹硪廊皇窃鷿L動(dòng)觸發(fā),渲染首先是預(yù)估高度,之后數(shù)據(jù)渲染后更新預(yù)估高度、更新占位div高度、更新偏移量。
另外就是對(duì)于滾動(dòng)事件做限制,如果滾動(dòng)高度恰好位于當(dāng)前元素范圍內(nèi)不做處理。
另外對(duì)于數(shù)據(jù)更新除了可以使用vue的生命周期函數(shù)updated還可以使用IntersectionObserver實(shí)現(xiàn)。
后期計(jì)劃:為了解決過(guò)快滑動(dòng)導(dǎo)致的白屏現(xiàn)象,會(huì)將不定高虛擬列表與虛擬滾動(dòng)結(jié)合。虛擬滾動(dòng)前幾天寫過(guò)一篇實(shí)現(xiàn)方案:虛擬滾動(dòng)實(shí)現(xiàn)
可優(yōu)化的方案:
- 采用多線程更新方法
this.updateItemsSize里內(nèi)容 - 使用css隱藏原生滾動(dòng)條,模擬出一個(gè)新滾動(dòng)條,人為控制新滾動(dòng)條的滾動(dòng)速度
.infinite-list-container::-webkit-scrollbar {
width:0;
}
本項(xiàng)目代碼地址:github.com/zhensg123/rareRecord/tree/main/virtual-list
以上就是vue實(shí)現(xiàn)不定高虛擬列表的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于vue虛擬列表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Vue實(shí)例對(duì)象的數(shù)據(jù)選項(xiàng)
下面小編就為大家?guī)?lái)一篇基于Vue實(shí)例對(duì)象的數(shù)據(jù)選項(xiàng)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
vue3-print-nb實(shí)現(xiàn)頁(yè)面打印(含分頁(yè)打印)示例代碼
大多數(shù)后臺(tái)系統(tǒng)中都存在打印的需求,在有打印需求時(shí),對(duì)前端來(lái)說(shuō)當(dāng)然是直接打印頁(yè)面更容易,下面這篇文章主要給大家介紹了關(guān)于vue3-print-nb實(shí)現(xiàn)頁(yè)面打印(含分頁(yè)打印)的相關(guān)資料,需要的朋友可以參考下2024-01-01
vue中v-model指令與.sync修飾符的區(qū)別詳解
本文主要介紹了vue中v-model指令與.sync修飾符的區(qū)別詳解,詳細(xì)的介紹了兩個(gè)的用法和區(qū)別,感興趣的可以了解一下2021-08-08
vue跳轉(zhuǎn)頁(yè)面打開新窗口,并攜帶與接收參數(shù)方式
這篇文章主要介紹了vue跳轉(zhuǎn)頁(yè)面打開新窗口,并攜帶與接收參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
flutter使用tauri實(shí)現(xiàn)一個(gè)一鍵視頻轉(zhuǎn)4K軟件
這篇文章主要為大家介紹了flutter使用tauri實(shí)現(xiàn)一個(gè)一鍵視頻轉(zhuǎn)4K軟件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
vue3+vite項(xiàng)目中顯示SVG圖片的實(shí)現(xiàn)
vite-plugin-svg-icons是一個(gè)Vite插件,其作用是將SVG圖標(biāo)文件轉(zhuǎn)換為Vue組件,本文主要介紹了vue3+vite項(xiàng)目中顯示SVG圖片的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02
使用Electron打包vue文件變成exe應(yīng)用程序的全過(guò)程
這篇文章主要給大家介紹了使用Electron打包vue文件變成exe應(yīng)用程序的全過(guò)程,文中通過(guò)代碼示例和圖文結(jié)合的方式給大家講解的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2024-01-01

