前言
最近在做一個官網,原本接口做的都是分頁的,但是客戶提出不要分頁,之前看過虛擬列表這個東西,所以進行一下了解。
為啥要用虛擬列表呢!
在日常工作中,所要渲染的也不單單只是一個li那么簡單,會有很多嵌套在里面。但數據量過多,同時渲染式,會在 渲染樣式 跟 布局計算上花費太多時間,體驗感不好,那你說要不要優化嘛,不是你被優化就是你優化它。
進入正題,啥是虛擬列表?
可以這么理解,根據你視圖能顯示多少就先渲染多少,對看不到的地方采取不渲染或者部分渲染。
這時候你完成首次加載,那么其他就是在你滑動時渲染,就可以通過計算,得知此時屏幕應該顯示的列表項。
怎么弄?
備注:很多方案對于動態不固定高度、網絡圖片以及用戶異常操作等形式處理的也并不好,了解下原理即可。
虛擬列表的實現,實際上就是在首屏加載的時候,只加載可視區域內需要的列表項,當滾動發生時,動態通過計算獲得可視區域內的列表項,并將非可視區域內存在的列表項刪除。
1、計算當前可視區域起始數據索引(startIndex)
2、計算當前可視區域結束數據索引(endIndex)
3、計算當前可視區域的數據,并渲染到頁面中
4、計算startIndex對應的數據在整個列表中的偏移位置startOffset并設置到列表上
由于只是對可視區域內的列表項進行渲染,所以為了保持列表容器的高度并可正常的觸發滾動,將Html結構設計成如下結構:
<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
infinite-list-container
為可視區域
的容器
infinite-list-phantom
為容器內的占位,高度為總列表高度,用于形成滾動條
infinite-list
為列表項的渲染區域
接著,監聽infinite-list-container
的scroll
事件,獲取滾動位置scrollTop
假定可視區域
高度固定,稱之為screenHeight
假定列表每項
高度固定,稱之為itemSize
假定列表數據
稱之為listData
假定當前滾動位置
稱之為scrollTop
則可推算出:
列表總高度listHeight
= listData.length * itemSize
可顯示的列表項數visibleCount
= Math.ceil(screenHeight / itemSize)
數據的起始索引startIndex
= Math.floor(scrollTop / itemSize)
數據的結束索引endIndex
= startIndex + visibleCount
列表顯示數據為visibleData
= listData.slice(startIndex,endIndex)
當滾動后,由于渲染區域
相對于可視區域
已經發生了偏移,此時我需要獲取一個偏移量startOffset
,通過樣式控制將渲染區域
偏移至可視區域
中。
時間分片
那么虛擬列表是一方面可以優化的方式,另一個就是時間分片。
先看看我們平時的情況
1.直接開整,直接渲染。
誒???我們可以發現,js運行時間為113ms,但最終 完成時間是 1070ms,一共是 js 運行時間加上渲染總時間。
PS:
在 JS 的 EventLoop
中,當JS引擎所管理的執行棧中的事件以及所有微任務事件全部執行完后,才會觸發渲染線程對頁面進行渲染
第一個 console.log
的觸發時間是在頁面進行渲染之前,此時得到的間隔時間為JS運行所需要的時間
第二個 console.log
是放到 setTimeout 中的,它的觸發時間是在渲染完成,在下一次 EventLoop
中執行的
那我們改用定時器
上面看是因為我們同時渲染,那我們可以分批看看。
let once = 20
let ul = document.getElementById('testTime')
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每頁最多20條
setTimeout(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loopRender(100000, 0)
這時候可以感覺出來渲染很快,但是如果渲染復雜點的dom會閃屏,為什么會閃屏這就需要清楚電腦刷新的概念了,這里就不詳細寫了,有興趣的小朋友可以自己去了解一下。
可以改用 requestAnimationFrame 去分批渲染,因為這個關于電腦自身刷新效率的,不管你代碼的事,可以解決丟幀問題。
let once = 20
let ul = document.getElementById('container')
// 循環加載渲染數據
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每頁最多20條
window.requestAnimationFrame(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)
還可以改用 DocumentFragment
什么是 DocumentFragment
DocumentFragment
,文檔片段接口,表示一個沒有父級文件的最小文檔對象。它被作為一個輕量版的 Document
使用,用于存儲已排好版的或尚未打理好格式的XML片段。最大的區別是因為 DocumentFragment
不是真實DOM樹的一部分,它的變化不會觸發DOM樹的(重新渲染) ,且不會導致性能等問題。
可以使用 document.createDocumentFragment
方法或者構造函數來創建一個空的 DocumentFragment
ocumentFragments
是DOM節點,但并不是DOM樹的一部分,可以認為是存在內存中的,所以將子元素插入到文檔片段時不會引起頁面回流。
當 append
元素到 document
中時,被 append
進去的元素的樣式表的計算是同步發生的,此時調用 getComputedStyle 可以得到樣式的計算值。而 append
元素到 documentFragment
中時,是不會計算元素的樣式表,所以 documentFragment
性能更優。當然現在瀏覽器的優化已經做的很好了, 當 append
元素到 document
中后,沒有訪問 getComputedStyle 之類的方法時,現代瀏覽器也可以把樣式表的計算推遲到腳本執行之后。
let once = 20
let ul = document.getElementById('container')
// 循環加載渲染數據
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每頁最多20條
window.requestAnimationFrame(_ => {
let fragment = document.createDocumentFragment()
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)
其實同時渲染十萬條數據這個情況還是比較少見的,就當做個了解吧。