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