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