vue實現(xiàn)不定高虛擬列表的示例詳解
虛擬列表主要解決大數(shù)據(jù)量數(shù)據(jù)一次渲染性能差的問題。
之前寫過一篇關于虛擬列表實現(xiàn)的文章:造輪子之不同場景下虛擬列表實現(xiàn),主要講了定高(高度統(tǒng)一和高度不統(tǒng)一兩種情況)虛擬列表的實現(xiàn),本文著重研究不定高虛擬列表的實現(xiàn)。在vue環(huán)境單頁面項目下研究實現(xiàn)。
前文講過虛擬列表的要做的事是確保性能的前提下,利用一定的技術模擬全數(shù)據(jù)一次性渲染后效果。
定高虛擬列表原理
綠色部分為containter,也就是父容器,它會有固定的高度。黃色部分為content,它是父容器的子元素。
當content的高度超過父容器的高度,就可以滾動內(nèi)容區(qū)了,這就是一般滾動原理。
虛擬列表需要使用這個滾動原理。虛擬列表使用占位div
,設置占位div
的高度為所有列表數(shù)據(jù)的高度進而撐開containter,形成滾動條。
然后虛擬列表具體渲染過程中,只是渲染可視區(qū)也就是父容器區(qū)域
至于可視區(qū)域的內(nèi)容滾動通過監(jiān)聽滾動條scroll
事件,獲取到滾動距離scrllTop
,轉換為可視區(qū)域的偏移位置,同時獲取渲染數(shù)據(jù)的起始和結束索引,渲染指定段數(shù)據(jù)形成假象的滾動。
不定高內(nèi)容數(shù)渲染
上一篇文章造輪子之不同場景下虛擬列表實現(xiàn)已經(jīng)給出了定高虛擬列表的實現(xiàn)。不定高相對定高的難點在于數(shù)據(jù)沒有渲染之前根本不知道數(shù)據(jù)的實際高度,解決方案理論上有
- 在屏幕外渲染,但消耗性能
- 以預估高度先行渲染,然后獲取真實高度并緩存
采用第一種方案顯然是不完美的,所以采用第二個方案,這也是之前有人實現(xiàn)過的。
不定高假數(shù)據(jù)
為了更接近業(yè)務,這里使用vue-codemirror方式渲染數(shù)據(jù),為vue-codemirror造假數(shù)據(jù)
function generateRandomNumber () { const min = 100 const max = 1000 // 生成隨機整數(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 // 確保長度在最小和最大范圍內(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條,具體是隨機生成的字符串,字符串長度100-1000,字符從A-Z中選取。
溫故定高虛擬列表
因為不定高虛擬列表有和定高虛擬列表相似之處,再來回顧一下之前定高(統(tǒng)一高度和不統(tǒng)一高度)的解決方案。這里只展示一下統(tǒng)一高度的,不統(tǒng)一高度的可以查看造輪子之不同場景下虛擬列表實現(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: () => [] }, // 每項高度 itemSize: { type: Number, default: 100 } }, computed: { // 列表總高度 listHeight () { return this.listData.length * this.itemSize }, // 可顯示的列表項數(shù) visibleCount () { return Math.ceil(this.screenHeight / this.itemSize) }, // 偏移量對應的style getTransform () { return `translate3d(0,${this.startOffset}px,0)` }, // 獲取真實顯示列表數(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, // 結束索引 end: null } }, methods: { scrollEvent () { // 當前滾動位置 const scrollTop = this.$refs.list.scrollTop // 此時的開始索引 this.start = Math.floor(scrollTop / this.itemSize) // 此時的結束索引 this.end = this.start + this.visibleCount // 此時的偏移量 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)一高度方式渲染
正如上面所說為了解決不定高內(nèi)容高度不定的問題,采用
以預估高度先行渲染,然后獲取真實高度并緩存方案
所以給每條假數(shù)據(jù)一條預估高度,然后使用定高虛擬列表渲染數(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>
設置codemirror
組件高度固定。查看一下效果
問題很明顯,由于codemirror
組件設置固定高度,導致渲染內(nèi)容擠到一起了,分不清哪個是哪個高度方向出現(xiàn)重合。所以預估高度不是這樣用的,預估高度的意義:它是一種高度占位
,是一種占位是務必要修正的。
修正高度
為了修正這個高度,需要等待數(shù)據(jù)渲染后拿到真實高度,這個需求可以在vue生命周期函數(shù)updated實現(xiàn),也可以通過IntersectionObserver實現(xiàn)。本文采用updated實現(xiàn)。
修正高度不僅修正每一條數(shù)據(jù)的高度,因為用來撐起可視區(qū)域的占位div
高度也是根據(jù)預估高度計算的,所以占位div
高度也需要更新,然后還需要更新偏移量。
具體在updated
里獲取真實元素大小,修改對應的尺寸緩存;更新占位div高度(使用計算屬性實現(xiàn));更新真實偏移量。
updated () { this.$nextTick(() => { // 獲取真實元素大小,修改對應的尺寸緩存 this.updateItemsSize() // 更新真實偏移量 this.setStartOffset() }) },
獲取數(shù)據(jù)實際高度,修改對應尺寸緩存
創(chuàng)建計算屬性_listData
拷貝列表數(shù)據(jù)。目的盡量不修改傳進來的listData
列表數(shù)據(jù),同時給渲染列表數(shù)據(jù)添加索引,實際是給渲染用的visibleCount
添加唯一索引
computed: { _listData () { return this.listData.reduce((init, cur, index) => { init.push({ // _轉換后的索引 _key: index, value: cur }) return init }, []) }, ... }
緩存每條數(shù)據(jù)的高度、以及數(shù)據(jù)坐標:用top
和bottom
標記
// 初始化緩存 initPositions () { this.positions = this._listData.map((d, index) => ({ index, height: this.itemSize, top: index * this.itemSize, bottom: (index + 1) * this.itemSize })) },
上面計算屬性_listData
以及緩存每條數(shù)據(jù)均是服務于這一步:獲取渲染數(shù)據(jù)實際高度,修改對應數(shù)據(jù)緩存尺寸
// 獲取實際高度,修正內(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ù)實際高度,修改對應尺寸緩存目的之一是為了更新列表總高度
computed: { ... // 列表總高度 listHeight () { return this.positions[this.positions.length - 1].bottom }, ... },
上述代碼中this.listHeight
是一個計算屬性,是占位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ù)據(jù)實際高度,修改對應尺寸緩存目的之二是為了更新真實偏移量。
借助this.positions
數(shù)組數(shù)據(jù),通過設置this.startOffset
,在傳導到計算屬性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: { ... // 偏移量對應的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 } }
滾動事件
滾動事件用以觸發(fā)更新
// 滾動事件 scrollEvent () { // 當前滾動位置 const scrollTop = this.$refs.list.scrollTop // 更新滾動狀態(tài) // 排除不需要計算的情況 if ( scrollTop > this.anchorPoint.bottom || scrollTop < this.anchorPoint.top ) { // 此時的開始索引 this.start = this.getStartIndex(scrollTop) // 此時的結束索引 this.end = this.start + this.visibleCount // 更新偏移量 this.setStartOffset() } }
其中this.anchorPoint
是計算屬性
computed: { ... anchorPoint () { return this.positions.length ? this.positions[this.start] : null } ... },
上述代碼中之所以排除不需要計算的情況,需要解釋一下。
真實的滾動就是滾動條滾動了多少,可視區(qū)就向上移動多少。但虛擬滾動不是。當起始索引發(fā)生變化時,渲染數(shù)據(jù)發(fā)生變化了,但渲染數(shù)據(jù)的高度不是連續(xù)的,所以需要動態(tài)的設置偏移量。當滾動時起始索引不發(fā)生變化時,因為數(shù)據(jù)變化是連續(xù)的,此時可以什么也不做,滾動顯示的內(nèi)容由瀏覽器控制。排除的部分就是索引沒發(fā)生變化的情況。
根據(jù)滾動高度獲取起始索引方法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)化
給滾動增加緩沖
,緩沖就是多渲染幾條,上方和下方渲染額外的數(shù)據(jù),比如前后多渲染2條。增加計算屬性aboveCount
和belowCount
,同時修改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) } },
存在問題
即便是給滾動增加緩沖,過快滑動時依然會出現(xiàn)白屏現(xiàn)象,究其本質是滾動過快而真實dom更新趕不上它
總結
本文主要研究了不定高虛擬列表的一種實現(xiàn)?;驹硪廊皇窃鷿L動觸發(fā),渲染首先是預估高度,之后數(shù)據(jù)渲染后更新預估高度、更新占位div高度、更新偏移量。
另外就是對于滾動事件做限制,如果滾動高度恰好位于當前元素范圍內(nèi)不做處理。
另外對于數(shù)據(jù)更新除了可以使用vue的生命周期函數(shù)updated還可以使用IntersectionObserver實現(xiàn)。
后期計劃:為了解決過快滑動導致的白屏現(xiàn)象,會將不定高虛擬列表與虛擬滾動結合。虛擬滾動前幾天寫過一篇實現(xiàn)方案:虛擬滾動實現(xiàn)
可優(yōu)化的方案:
- 采用多線程更新方法
this.updateItemsSize
里內(nèi)容 - 使用css隱藏原生滾動條,模擬出一個新滾動條,人為控制新滾動條的滾動速度
.infinite-list-container::-webkit-scrollbar { width:0; }
本項目代碼地址:github.com/zhensg123/rareRecord/tree/main/virtual-list
以上就是vue實現(xiàn)不定高虛擬列表的示例詳解的詳細內(nèi)容,更多關于vue虛擬列表的資料請關注腳本之家其它相關文章!
相關文章
搭建Vue從Vue-cli到router路由護衛(wèi)的實現(xiàn)
這篇文章主要介紹了搭建Vue從Vue-cli到router路由護衛(wèi)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11vue3如何定義變量及ref、reactive、toRefs特性說明
這篇文章主要介紹了vue3如何定義變量及ref、reactive、toRefs特性說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教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問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04