技術不侷限於框架,相同的原理只是實現方式略有不同。
前置
1. 什麼是虛擬列表?
首先,虛擬列表只是一個概念,本人對虛擬列表這個表述不置可否。
虛擬列表是對於列表形態資料展示的一種按需渲染,是對長列表渲染的一種優化。
虛擬列表不會一次性完整地渲染長列表,而是按需顯示的一種方案,以提高無限滾動的效能。
2. 虛擬列表的實現原理?
根據容器元素的高度 clientHeight
以及列表項元素的高度 offsetHeight
來顯示長列表資料中的某一個部分,而不是去完整地渲染整個長列表。
實現一個虛擬列表需要:
- 得知容器元素的高度
clientHeight
- 得知列表項元素的高度
offsetHeight
- 計算可視區域應該渲染的列表項的個數
count = clientHeight / offsetHeight
- 計算可視區域資料渲染的起始位置
start
- 計算可視區域資料渲染的結束位置
end
- 對完整長列表資料進行截斷
sliceList = dataList.slice(start, end)
- 渲染截斷後的列表資料,進而實現無限載入
3. 虛擬列表與懶載入有何不同?
懶載入與虛擬列表其實都是延時載入的一種實現,原理相同但場景略有不同。
- 懶載入的應用場景偏向於網路資源請求,解決網路資源請求過多時,造成的網站響應時間過長的問題。
- 虛擬列表是對長列表渲染的一種優化,解決大量資料渲染時,造成的渲染效能瓶頸的問題。
4. IntersectionObserver 介紹
IntersectionObserver 提供了一種非同步觀察目標元素與視口的交叉狀態,簡單地說就是能監聽到某個元素是否會被我們看到,當我們看到這個元素時,可以執行一些回撥函式來處理某些事務。
let io = new IntersectionObserver(callback, option);
callback
會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另一次是完全離開視口(開始不可見)。
實現
1. 模擬十萬條資料:
function getDataList() {
let data = []
for(let i = 0; i < 100000; i++) {
data.push({id: "item" + i, value: Math.random() * i})
}
return data;
}
2. Dom 建立及列表渲染:
不依賴框架的情況下,需要命令性的去建立 DOM 以及操作 DOM
。
<ul class="container">
<span class="sentinels">....</span>
</ul>
function $(selector) {
return document.querySelector(selector)
}
function loadData(start, end) {
// 擷取資料
let sliceData = getDataList().slice(start, end)
// 現代瀏覽器下,createDocumentFragment 和 createElement 的區別其實沒有那麼大
let fragment = document.createDocumentFragment();
for(let i = 0; i < sliceData.length; i++) {
let li = document.createElement('li');
li.innerText = JSON.stringify(sliceData[i])
fragment.appendChild(li);
}
$('.container').insertBefore(fragment, $('.sentinels'));
}
如果是基於 Virtual DOM
的框架,直接運算元據即可(虛擬碼):
// 父元件
<virtual-list :listData="listData"></virtual-list>
// 子元件
<ul class='container'>
<li
v-for="item in sliceData"
:key="item.id"
>{{ item }}</li>
</ul>
...
// js
this.sliceData = this.data.slice(start, index)
3. 使用 IntersectionObserver API
建立監聽器:
let count = Math.ceil(document.body.clientHeight / 120);
let startIndex = 0;
let endIndex = 0;
...
let io = new IntersectionObserver(function(entries) {
loadData(startIndex, count)
// 標誌位元素進入視口
if(entries[0].isIntersecting) {
// 更新列表資料起始和結束位置
startIndex = startIndex += count;
endIndex = startIndex + count;
if(endIndex >= getDataList().length) {
// 資料載入完取消觀察
io.unobserve(entries[0].target)
}
// requestAnimationFrame 由系統決定回撥函式的執行時機
requestAnimationFrame(() => {
loadData(startIndex, endIndex)
let num = Number(getDataList().length - startIndex)
let info = ['還有', num , '條資料']
$('.top').innerText = info.join(' ')
if(num - count <= 0) {
$('.top').classList.add('out')
}
})
}
});
// 開始觀察“標誌位”元素
io.observe($('.sentinels'));
})
由於 IntersectionObserver
無法監聽動態建立的 dom
,所以我們設定一個「標誌位」元素 span.sentinels
作為監聽的目標物件。
<ul class="container">
<span class="sentinels">....</span>
</ul>
如果目標元素正處於交叉狀態 entries[0].isIntersecting == true
,則代表 .sentinels
進入了可視區域,從而載入新的列表資料。
if(entries[0].isIntersecting) {
...
requestAnimationFrame(() => {
loadData(startIndex, endIndex)
})
...
}
最後將新的列表 insertBefore
到其前面,進而實現無限載入。
$('.container').insertBefore(fragment, $('.sentinels'));
同系列文章: