vue實(shí)現(xiàn)不定高虛擬列表的示例詳解
虛擬列表主要解決大數(shù)據(jù)量數(shù)據(jù)一次渲染性能差的問題。
之前寫過(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)容高度不定的問題,采用
以預(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
組件高度固定。查看一下效果
問題很明顯,由于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) } },
存在問題
即便是給滾動(dòng)增加緩沖,過(guò)快滑動(dòng)時(shí)依然會(huì)出現(xiàn)白屏現(xiàn)象,究其本質(zhì)是滾動(dòng)過(guò)快而真實(shí)dom更新趕不上它
總結(jié)
本文主要研究了不定高虛擬列表的一種實(shí)現(xiàn)。基本原理依然是原生滾動(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)文章!
- 不同場(chǎng)景下Vue中虛擬列表實(shí)現(xiàn)
- 詳解vue3中虛擬列表組件的實(shí)現(xiàn)
- Vue中虛擬列表的原理與實(shí)現(xiàn)詳解
- 基于Vue實(shí)現(xiàn)封裝一個(gè)虛擬列表組件
- vue長(zhǎng)列表優(yōu)化之虛擬列表實(shí)現(xiàn)過(guò)程詳解
- 結(jié)合康熙選秀講解vue虛擬列表實(shí)現(xiàn)
- Vue 虛擬列表的實(shí)戰(zhàn)示例
- vue實(shí)現(xiàn)虛擬列表功能的代碼
- 使用 Vue 實(shí)現(xiàn)一個(gè)虛擬列表的方法
- vue簡(jiǎn)單實(shí)現(xiàn)一個(gè)虛擬列表的示例代碼
相關(guān)文章
搭建Vue從Vue-cli到router路由護(hù)衛(wèi)的實(shí)現(xiàn)
這篇文章主要介紹了搭建Vue從Vue-cli到router路由護(hù)衛(wèi)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Vue的路由配置過(guò)程(Vue2和Vue3的路由配置)
這篇文章回顧了Vue2和Vue3中路由的配置步驟,包括安裝正確的路由版本、創(chuàng)建路由實(shí)例、配置routes以及在入口文件中注冊(cè)路由,Vue2中使用Vue.use(VueRouter),而Vue3中使用createRouter和createWebHashHistory2025-01-01vue axios請(qǐng)求成功卻進(jìn)入catch的原因分析
這篇文章主要介紹了vue axios請(qǐng)求成功卻進(jìn)入catch的原因分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09vscode 開發(fā)Vue項(xiàng)目的方法步驟
這篇文章主要介紹了vscode 開發(fā)Vue項(xiàng)目的方法步驟,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11vue3如何定義變量及ref、reactive、toRefs特性說(shuō)明
這篇文章主要介紹了vue3如何定義變量及ref、reactive、toRefs特性說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06解決VUE-npm ERR! C:\rj\node-v14.4.0-win-x64\nod問題
這篇文章主要介紹了解決VUE-npm ERR! C:\rj\node-v14.4.0-win-x64\nod問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04